From d33f0ad2f80da0abe01b7ee8daffc98c19d0457f Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Sun, 4 Oct 2026 23:35:33 +0530 Subject: [PATCH 01/37] Add CLAUDE.md with commands, architecture and platform notes Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c371fb7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,178 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Riko is a local desktop AI companion with a VRM avatar. +- **Python backend** (`Code/`, FastAPI/uvicorn on `127.0.0.1:8765`) owns conversation, in-process llama.cpp inference, microphone capture/ASR, TTS playback, memory, tools and persistence. Core package: `Code/process/app_core/` (written `app_core/` below). Entry points: `Code/run_server.py` (backend), `Code/desktop_server.py` (FastAPI app), `Code/discord_bot.py`, `Code/task_mcp_server.py`. +- **Electron + React + three.js** (`electron/`) owns every window, the avatar and the UI, and talks to the backend only over HTTP/WebSocket. + +This layout exists on the `rewrite-2` branch. `origin/llama.cpp` is an older pre-rewrite tree whose tip fails to parse (`Code/process/llm_scripts/module.py:238`). `old llm_scripts/` here is dead code (the space in its name makes it unimportable). + +## Commands + +No linter or formatter is configured. Besides tests, the README's only checks are `node --check main.cjs` and `node --check preload.cjs` in `electron/`. + +```bash +# Python >= 3.11 (CI uses 3.11). Tests need the full runtime deps; `pip install -e .[test]` alone fails collection. +python -m pip install -r requirements-runtime.txt +python -m pip install --no-deps EfficientWord-Net +# Linux CI pre-installs CPU torch: python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +# install_reqs.sh pins CUDA 13 (cu130) torch wheels, which do not exist for macOS. + +python -u Code/run_server.py # backend; run from the repo root; Ctrl+C stops it + +# Python tests, from the repo root (pyproject sets pythonpath=Code, testpaths=tests) +HF_HUB_OFFLINE=1 python -m pytest -q +python -m pytest tests/test_tasks.py -q +python -m pytest tests/test_tasks.py::test_real_stdio_mcp_handshake_and_task_round_trip -q +RIKO_RELEASE_BUILD=1 python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py -q # everything CI runs + +# Electron, from electron/ (CI uses Node 22) +npm ci && npm run build && npm run start # loads dist/; start the backend first +npm run dev # Vite on :5173, then in another shell: RIKO_DEV=1 npm run start +npm test # node --test src/*.test.mjs +node --test src/release.test.mjs # one file +node --test --test-name-pattern='refuses overwrite' src/release.test.mjs # one test + +python -u Code/task_mcp_server.py --store persistent_memories/tasks.sqlite3 # optional stdio MCP server for external clients +``` + +Test caveats: +- Use `HF_HUB_OFFLINE=1`: otherwise `tests/test_emotion.py` downloads Julia-1 (unpinned) and imports code from it. +- `tests/test_server_logging.py` is stale (2 failures). `tests/test_settings_store.py::test_current_character_configuration_has_valid_settings` needs the private, gitignored `character_config.yaml` and only skips under `RIKO_RELEASE_BUILD=1`. +- `tests/test_runtime_awareness.py::test_priority_still_allows_sustained_interruption_and_explicit_stop` can hang intermittently (see the lock rule under Threading); `-o faulthandler_timeout=60` dumps stacks. +- Some tests use cwd-relative paths (`Path('Code')`, `Path('.')`), and `electron/src/formatted_text.test.mjs` / `polished_ui.test.mjs` start Vite SSR servers, so keep the working directories above. +- Every GPU and audio-device path is mocked; a green suite says nothing about CUDA, Metal or ROCm behaviour. + +### Native llama.cpp bridge + +`tools/llama_cpp/README.md` (PowerShell-oriented) is the reference. Check out llama.cpp at `b92761a515ea31e852e7fbc1fad5f874b46f3718`, run `git apply --check` and then `git apply` with `tools/llama_cpp/emotion-probe.patch`, then: + +```bash +cmake -S .native/llama.cpp -B .native/llama.cpp/build -DGGML_CUDA=ON -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DRIKO_NATIVE_BRIDGE_SOURCE="$PWD/tools/llama_cpp/riko-native.cpp" +cmake --build .native/llama.cpp/build --config Release --target riko-native -j 2 +``` + +`-DGGML_CUDA=OFF` gives a CPU-only build on Windows/Linux. On macOS the same configure builds Metal by default (embedded shader library, no full Xcode needed) and produces `libriko-native.dylib`. Point `runtime.native_library` at the built library and keep its llama/ggml libraries beside it. + +Release (CI only, `.github/workflows/release.yml`): `python tools/release/build.py` clones llama.cpp into `.native/llama.cpp-release` (it refuses if that exists), builds the `cuda` and `vulkan` backends, freezes `riko-backend` with PyInstaller into `release-stage/` and runs `--release-check`; electron-builder 26.0.12 then packages from `electron/`. + +## Architecture + +### Process model +- **Dev:** backend and Electron start separately; Electron never starts or stops Python. **Packaged:** Electron spawns and stops the frozen backend. +- Python child processes: a fresh interpreter per built-in tool call (`app_core/tools/worker.py`, `--tool-worker` when frozen), stdio MCP servers from `mcp.json`, and the Discord client (`--discord-worker` when frozen). GPT-SoVITS is always a separate HTTP server. +- Every Electron window loads the same Vite bundle, routed by URL hash in `electron/src/main.jsx`: `#/overlay` (transparent click-through avatar layer), `#/control` (chat/settings; "closing" collapses it to a dock), `#/whiteboard`, `#/effects`, `#/setup` (packaged first run), `#/neural-data`. +- One `electron/preload.cjs` exposes every `contextBridge` API to every window. The `ipcMain` handlers in `electron/main.cjs` check that `event.sender` owns the request; new handlers must do the same. + +### Startup and service assembly +- `run_server.main` chdirs to `$RIKO_DATA_DIR` (default: repo root), loads config, configures logging, then imports `desktop_server`. That import itself calls `load_config()` and builds `DiscordLauncher`, `GPUMonitor` and `WhiteboardImages`, so tests stub `load_config` before the first import (see `tests/test_desktop_api.py`). +- The FastAPI lifespan calls `create_chat_service` (`app_core/factory.py`), which builds everything inside an `ExitStack` that rolls back on failure: `ActionController` → optional Julia emotion engine → `create_provider` (+ optional emotion probe) → `provider.warmup()` (loads the GGUF, no timeout) → `TaskStore` / `ToolRegistry` → `MemoryStore` → `ChatService`. Then come `SessionManager`, `warm_session` (ASR/VAD/wake/TTS, bounded by `runtime.startup_timeout_seconds`), `ConversationStore`, `Initiative` and the resource/task-file watchers, and finally `runtime.ready` is published. +- uvicorn binds :8765 only after lifespan startup, so the API refuses connections while the model loads. +- With `desktop.setup_on_startup_error: true`, a failing `create_chat_service` leaves only the settings/status/resource/media routes up (the rest return 503) so Settings can repair the YAML. +- Shutdown: stop Discord → `close_bounded(session, 10)` (`app_core/runtime/lifecycle.py`; a hung close is abandoned to a daemon thread) → unwind the ExitStack. + +### Threading and events +- `event_bus` (`app_core/events/bus.py`) is synchronous: listeners run on the publisher's thread, their exceptions are swallowed, and every event carries a monotonic `sequence`. +- `SessionManager` (`app_core/runtime/session.py`) serialises foreground turns with `_turn_lock`. A second turn raises `RuntimeError('Riko is already handling another turn')`, which `app_core/integrations/discord/api.py` string-matches into a 409. The `_voice_lock` RLock guards voice/playback state, and the session's bus listener takes it for every event. **Never publish on the bus while holding another lock**: `WakeWord` does so today and has a reproducible lock-order inversion with `_voice_lock`. +- Hidden coupling: voice input, animation, initiative, Discord and the server read private `SessionManager` fields (`_voice_lock`, `_active_turn`, `_generation_active`). Attributes are monkey-patched onto `ChatService` and providers and read back through `getattr` defaults. Renames break silently. +- Background work uses `DaemonExecutor` (`app_core/runtime/workers.py`; bounded queue, daemon threads). Each turn is cancelled through its own `threading.Event` and `TurnCancelled`. + +### Inference +- `create_provider` (`app_core/inference/providers.py`) picks the provider: + - `llama_cpp` → `InProcessLlamaProvider` (`llama_native.py`). It calls the C ABI in `tools/llama_cpp/riko-native.cpp` via ctypes: five `riko_*` exports, serving only `/v1/responses`, `/apply-template`, `/tokenize`, `/props` and `/slots`, with argv from `native_arguments` in `llama_context.py`. There is no HTTP listener, llama-server or llama-cpp-python, and no fallback: `runtime.native_library` is required. + - `openai`, `lm_studio`, `openai_compatible`, `ollama`, `local_http` → `OpenAIProvider`: Responses API with a Chat Completions fallback, byte-based token estimates, no slots, no provider-level cancel. +- `SlotScheduler` (`llama_context.py`) reserves slot 0 for the `live` lane. `initiative` and `reflection` share slots 1..N-1 (`runtime.parallel_slots`, 2–4), and live turns preempt them when `runtime.pause_background_on_live` is set. The emotion probe captures only on slot 0. +- `--ctx-size` comes from `kv_budget.pool_capacity`. Per-role budgets are enforced only by Python-side packing (`context_budget.pack_context`, with exact native token counts) and by `max_output_tokens`. +- Turn flow: `SessionManager.respond` → `ChatService.respond` (`app_core/conversation/chat.py`): + 1. memory capture and recall; + 2. the prompt: a stable system + history prefix (so the KV cache is reused), then `context_kind='optional'` system messages; + 3. `provider.generate`, which streams deltas; + 4. the `ToolRegistry` loop, up to `tools.max_iterations`; + 5. save history. +- The native context is allocated once, so changing `n_ctx`, `parallel_slots`, the KV pool or the initiative/reflection budgets needs a Python restart. A native crash kills the whole backend. +- The llama.cpp pin and the patch's five-file footprint are enforced by `tests/test_release_build.py`. + +### Voice pipeline +- The mic opens only on `/api/voice/start|activate` or `/api/mic/toggle`. `VoiceInput` (`app_core/audio/voice_input.py`) runs these threads: + 1. `microphone-capture`: sounddevice, 16 kHz mono int16, 512-sample frames. + 2. `voice-vad`: Silero VAD, `WakeWord` (EfficientWord-Net ONNX on CPU) and `VoiceSegments`. + 3. `voice-asr`: faster-whisper. + 4. `voice-turn`: calls `SessionManager.respond`. +- Output: streamed deltas → `SpeechChunks` (`speech.max_words`) → `SpeechQueue` (`app_core/audio/speech.py`) → GPT-SoVITS over HTTP (`sovits_ping_config.url`) → raw PCM16 played by sounddevice. `sovits_ping_config.sample_rate` must match the server. +- Barge-in uses the character offsets of spoken chunks. `Interjections` cancels after `voice.interruption_seconds`, and history is rewritten at the estimated cut point. There is no echo cancellation. + +### Persistence +The data root is the directory holding `character_config.yaml`. +- `persistent_memories/` is user data: never delete it during cleanup. Runtime files: + - `chat_history.json`: the model's context. Both `ChatService` and `SessionManager` rewrite the whole file, and an invalid file is treated as empty. + - `memory_store.json` (`app_core/persistence/memory.py`): written via temp file, fsync and replace; fails closed. Its FAISS index is written but rebuilt from embeddings on load. + - `conversations.sqlite3`: the UI archive, built from bus events. It is a second history and can diverge from `chat_history.json`. + - `tasks.sqlite3` (`app_core/persistence/tasks.py`): revision-checked, and shared with `Code/task_mcp_server.py`. + - The whiteboard, desktop, initiative, tool-approval and Discord JSON files, plus `wake_words/`. +- Also under the data root: `logs/debug.log` (rotating, secrets redacted), and emotion-probe datasets and weights under `models/` (`app_core/emotion/probe_storage.py`). + +### Configuration and settings +- `load_config` (`app_core/configuration/config.py`) reads `$RIKO_CONFIG`, or `./character_config.yaml`, with PyYAML; a missing file silently yields defaults (provider `openai`). Relative paths resolve against the YAML's directory (`AppConfig.root`). Only `runtime`, `tools`, `memory` and `emotion` are typed dataclasses; other sections are read from `config.raw`. +- `character_config.yaml` is gitignored and no template is committed. `electron/main.cjs` also parses it at launch and aborts if it is missing. +- The Settings UI goes through `SettingsStore` (`app_core/configuration/settings_store.py`, module-level `LOCK`): + - GET returns values, fields and a sha256 `revision`. + - Validate writes a temporary `.settings-validation-*.yaml` beside the config and runs `load_config` on it. + - PUT re-checks the revision (409 on conflict), keeps `character_config.yaml.previous`, and writes via mkstemp + fsync + `os.replace`. ruamel preserves comments and unknown keys. +- Only keys already in the YAML or defaulted in `SettingsStore._values` are editable, and each type is inferred from the current value. To add a setting: + 1. Give it a default in `_values`, plus entries in `ENUMS`, `RANGES`, `HELP` and `LABELS`. + 2. For it to apply without a restart, also update the restart flags in `field()`, `restart_required` in `save()`, and `save_settings` in `Code/desktop_server.py`. +- Applied live: `avatar.*`, `runtime.pause_background_on_live`, and `emotion.probe.interval_tokens` (if the probe was enabled at load). Everything else needs a Python restart. Electron reads `desktop.shortcuts`, `desktop.debug` and `presets.default.name` only at launch. `persistent_memories/initiative_settings.json` overrides the YAML `initiative` section. +- The backend's PyYAML parses YAML 1.1; ruamel and Electron's `yaml` parse 1.2. They disagree on `yes`/`no`/`on`/`off`. + +### Electron ↔ backend contract +- `http://127.0.0.1:8765` is hard-coded in `electron/src/api.mjs`, `electron/main.cjs` and several components. There is no authentication; CORS allows `null` (the packaged `file://` renderer) and the Vite origin. +- `/ws/events` (`app_core/events/stream.py`) sends `state.snapshot`, then `resource.snapshot`, then every bus event. A client with 100 queued events is closed with code 1013. +- All windows share one socket (`electron/src/event_connection.mjs`, 1 s reconnect). Subscribe via `connectEvents`, `useRuntime` or `useResource`, and never poll: `electron/src/subscription_guard.test.mjs` forbids `setInterval`. +- Every `DesktopState` change republishes a full `state.snapshot`. Panels read `resource.` events from `ResourceEvents` (`app_core/events/resources.py`); a new topic needs an `observe()` mapping and a `resource_getters()` entry. +- `POST /api/chat` returns only after the full reply; deltas arrive over the socket. Renderer acknowledgements are authoritative (`/api/surfaces/result`, `/api/avatar/animation/result`). + +### Discord +`Code/discord_bot.py` is a separate process and only a backend client; it loads no model. It holds one WebSocket at `/ws/discord/client?instance=` and calls `/api/discord/*` (`app_core/integrations/discord/api.py`). Start it with `POST /api/discord/start` or the tray menu, or by hand once the backend runs. Credentials go in a local `.env` (template: `.env.discord.example`). + +### Dev vs packaged +- **Dev:** the repo root is both the cwd and the data root. +- **Packaged, first launch:** Electron shows `#/setup`, which writes `/character_config.yaml` (never overwriting) and `data-location.json` in Electron's userData (`electron/release.cjs`). +- **Packaged, later launches:** + - Electron spawns `backend/riko-backend` with `RIKO_MANAGED=1`, `RIKO_DATA_DIR`, `RIKO_CONFIG` and `RIKO_BUNDLE_ROOT`. + - Hugging Face and torch caches go under `/models`. + - A `shutdown` line or EOF on stdin stops the backend; Electron kills it after 15 s. + - `runtime.native_library: bundled:` resolves to `$RIKO_BUNDLE_ROOT/native//`. + +## Conventions and gotchas +- Writes go to a temp file and then `os.replace`; the memory store and settings also fsync. Stores fail closed rather than resetting user data. +- Optimistic concurrency uses revisions throughout: the settings `revision`, task `expected_revision` (`TaskConflict` → 409), the Discord access revision, memory-record revisions, and whiteboard `board_revision`. +- File paths supplied by the renderer or the model must go through `resolve_media` (`app_core/desktop/media.py`: approved roots plus an extension allowlist). +- Heavy dependencies (torch, faster_whisper, sounddevice, silero_vad, sentence_transformers, openai, huggingface_hub) are imported lazily, and tests swap them through `sys.modules`. Services accept `start=False` / `start_worker=False`. Tests must close services and unsubscribe from the global `event_bus` in `finally`. +- The `app_core/` root may contain only `__init__.py` and `factory.py`, and every relative import must resolve (`tests/test_app_core_structure.py`). `app_core/__init__.py` eagerly imports `SessionManager`, audio and emotion. +- Built-in tools (`app_core/tools/builtin/`) run in an isolated subprocess. `mcp.json` (copied from `mcp.json.example`) loads only if `tools.mcp_config` points at it. Registering a tool name twice silently overwrites the first. +- Electron: + - Pure logic lives in `.mjs`/`.cjs` files, each with a sibling `*.test.mjs`. + - Several tests regex-match the source text of `electron/main.cjs`, the CSS and some components, so refactors must update those tests too. + - `electron/vite.config.mjs` passes `react` without calling it, so JSX uses the classic transform and every `.jsx` must `import React`. +- Style: the Python and JS are dense, often with several statements per line; match the surrounding code. +- The README links to `docs/*.md`, but `/docs` is gitignored and absent. +- `tools/migrate_legacy_memories.py` and `tools/migrate_legacy_chat_history.py` only dry-run unless given `--write`; run them with the app stopped. + +## Platform status + +Releases ship CUDA and Vulkan bundles for Windows and Linux x64 only; nothing builds, packages, detects or tests Metal or ROCm. The Python inference layer is backend-agnostic: the backend is whatever llama.cpp build `runtime.native_library` loads. The patched bridge, including the emotion probe, has been built and run on Apple Silicon Metal at the current pin. + +Backend and device assumptions live here: +- **Native build and packaging:** + - `tools/release/build.py`: only cuda/vulkan; collects only `.dll`/`.so` files; uses an `$ORIGIN` rpath, which macOS dyld ignores (macOS needs `@loader_path`). + - `.github/workflows/release.yml`: windows-2022 and ubuntu-22.04, CUDA 12.4.1. + - `electron/electron-builder.yml`: NSIS, AppImage and deb. +- **Bundled-backend allowlists and library names:** `app_core/configuration/config.py`, `electron/release.cjs` (detects GPUs via nvidia-smi and vulkaninfo) and `electron/src/first_setup.jsx`. They are asserted by `tests/test_release_config.py` and `electron/src/release.test.mjs`. +- **Library loading:** `os.add_dll_directory` is called only on Windows (`app_core/inference/llama_native.py`, `Code/run_server.py`). +- **NVIDIA-only telemetry:** `app_core/resources/gpu_memory.py` (nvidia-smi, DXGI vendor `0x10de`, PowerShell counters) and the CUDA rows in `app_core/resources/vram_estimate.py`. +- **ASR defaults:** `device='cuda'` and `compute_type='int8_float16'` are set in `app_core/audio/voice_input.py`, `app_core/runtime/warmup.py`, `app_core/integrations/discord/api.py` and `app_core/configuration/settings_store.py`. They fail on macOS, where CTranslate2 has no GPU backend; `auto` + `default` works on CPU. +- **Julia devices:** the allowlist is `cpu`/`cuda`/`cuda:0`, with no `mps`. It lives in `app_core/emotion/julia.py` and the settings `ENUMS`, and `tests/test_llama_native.py` asserts it. +- **Windows-only behaviour:** `WindowsActivity` in `app_core/runtime/initiative.py`, the Win32 pointer hooks in `electron/main.cjs`, and the acrylic material in `electron/window_material.cjs`. From 65238e9f9b6ce72a8350092466120ade1cc924c3 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Sun, 4 Oct 2026 23:39:58 +0530 Subject: [PATCH 02/37] Update CLAUDE.md for the main branch rename Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c371fb7..ca12a74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Riko is a local desktop AI companion with a VRM avatar. - **Python backend** (`Code/`, FastAPI/uvicorn on `127.0.0.1:8765`) owns conversation, in-process llama.cpp inference, microphone capture/ASR, TTS playback, memory, tools and persistence. Core package: `Code/process/app_core/` (written `app_core/` below). Entry points: `Code/run_server.py` (backend), `Code/desktop_server.py` (FastAPI app), `Code/discord_bot.py`, `Code/task_mcp_server.py`. - **Electron + React + three.js** (`electron/`) owns every window, the avatar and the UI, and talks to the backend only over HTTP/WebSocket. -This layout exists on the `rewrite-2` branch. `origin/llama.cpp` is an older pre-rewrite tree whose tip fails to parse (`Code/process/llm_scripts/module.py:238`). `old llm_scripts/` here is dead code (the space in its name makes it unimportable). +This layout is on `main` (formerly the `rewrite-2` branch). `origin/llama.cpp` is an older pre-rewrite tree, kept for reference, whose tip fails to parse (`Code/process/llm_scripts/module.py:238`). `old llm_scripts/` here is dead code (the space in its name makes it unimportable). ## Commands From db157890593a5569621b1b91733a29d5faea8a4f Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Sun, 4 Oct 2026 23:56:35 +0530 Subject: [PATCH 03/37] Stop publishing events while holding component locks Bus listeners run on the publisher's thread and take SessionManager._voice_lock, so WakeWord, ActionController, Initiative, ResourceEvents, EmotionProbe, the speech queue and the Discord launcher could each deadlock against it. - Add events/outbox.py: queue events/callbacks under a lock, flush after release, delivered in order by a single drainer. - Move every publish/callback out of component locks; ResourceEvents drops superseded values so a stale snapshot is never published last. - _playback_event ignores non voice/speech events before taking _voice_lock. - EventBus logs listener exceptions instead of swallowing them. - tests/test_lock_discipline.py: static scan for publish-under-lock plus regression tests for the reproduced WakeWord and ActionController deadlocks. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- Code/desktop_server.py | 2 +- Code/process/app_core/audio/speech.py | 33 ++-- Code/process/app_core/audio/wake_word.py | 11 +- Code/process/app_core/emotion/probe.py | 5 +- Code/process/app_core/events/bus.py | 7 +- Code/process/app_core/events/outbox.py | 35 ++++ Code/process/app_core/events/resources.py | 15 +- .../app_core/integrations/discord/launcher.py | 28 ++- Code/process/app_core/runtime/actions.py | 63 ++++--- Code/process/app_core/runtime/initiative.py | 2 +- Code/process/app_core/runtime/session.py | 5 + tests/test_lock_discipline.py | 166 ++++++++++++++++++ 13 files changed, 313 insertions(+), 62 deletions(-) create mode 100644 Code/process/app_core/events/outbox.py create mode 100644 tests/test_lock_discipline.py diff --git a/CLAUDE.md b/CLAUDE.md index ca12a74..d871737 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,6 @@ python -u Code/task_mcp_server.py --store persistent_memories/tasks.sqlite3 # Test caveats: - Use `HF_HUB_OFFLINE=1`: otherwise `tests/test_emotion.py` downloads Julia-1 (unpinned) and imports code from it. - `tests/test_server_logging.py` is stale (2 failures). `tests/test_settings_store.py::test_current_character_configuration_has_valid_settings` needs the private, gitignored `character_config.yaml` and only skips under `RIKO_RELEASE_BUILD=1`. -- `tests/test_runtime_awareness.py::test_priority_still_allows_sustained_interruption_and_explicit_stop` can hang intermittently (see the lock rule under Threading); `-o faulthandler_timeout=60` dumps stacks. - Some tests use cwd-relative paths (`Path('Code')`, `Path('.')`), and `electron/src/formatted_text.test.mjs` / `polished_ui.test.mjs` start Vite SSR servers, so keep the working directories above. - Every GPU and audio-device path is mocked; a green suite says nothing about CUDA, Metal or ROCm behaviour. @@ -76,7 +75,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. ### Threading and events - `event_bus` (`app_core/events/bus.py`) is synchronous: listeners run on the publisher's thread, their exceptions are swallowed, and every event carries a monotonic `sequence`. -- `SessionManager` (`app_core/runtime/session.py`) serialises foreground turns with `_turn_lock`. A second turn raises `RuntimeError('Riko is already handling another turn')`, which `app_core/integrations/discord/api.py` string-matches into a 409. The `_voice_lock` RLock guards voice/playback state, and the session's bus listener takes it for every event. **Never publish on the bus while holding another lock**: `WakeWord` does so today and has a reproducible lock-order inversion with `_voice_lock`. +- `SessionManager` (`app_core/runtime/session.py`) serialises foreground turns with `_turn_lock`. A second turn raises `RuntimeError('Riko is already handling another turn')`, which `app_core/integrations/discord/api.py` string-matches into a 409. The `_voice_lock` RLock guards voice/playback state, and bus listeners can take it. Lock order is `_capture_lock` (request handlers only) → `_voice_lock` → component locks. **Never publish or run callbacks while holding a component lock**: queue them on an `Outbox` (`app_core/events/outbox.py`) and call `flush()` after releasing it. `tests/test_lock_discipline.py` enforces this with a static scan and deadlock regressions. - Hidden coupling: voice input, animation, initiative, Discord and the server read private `SessionManager` fields (`_voice_lock`, `_active_turn`, `_generation_active`). Attributes are monkey-patched onto `ChatService` and providers and read back through `getattr` defaults. Renames break silently. - Background work uses `DaemonExecutor` (`app_core/runtime/workers.py`; bounded queue, daemon threads). Each turn is cancelled through its own `threading.Event` and `TurnCancelled`. diff --git a/Code/desktop_server.py b/Code/desktop_server.py index e9a74e6..a107e13 100644 --- a/Code/desktop_server.py +++ b/Code/desktop_server.py @@ -381,7 +381,7 @@ def save_settings(request: SettingsPatch): with LOCK: config.avatar = deepcopy(load_config(settings_store().path).avatar) config.raw['avatar'] = deepcopy(config.avatar) - event_bus.publish('state.snapshot', **snapshot()) + event_bus.publish('state.snapshot', **snapshot()) initiative = getattr(session, 'initiative', None) if result.get('saved') and initiative: budgets = {key.split('.')[-1]: result['values'][key] for key in ('initiative.context_window_tokens', 'initiative.max_output_tokens')} diff --git a/Code/process/app_core/audio/speech.py b/Code/process/app_core/audio/speech.py index 810ea11..9473271 100644 --- a/Code/process/app_core/audio/speech.py +++ b/Code/process/app_core/audio/speech.py @@ -47,17 +47,19 @@ def submit(self, text, turn_id, start_offset=0, end_offset=None): with self._submit_lock: if self.closed.is_set(): return False if self._failed_turn == turn_id: return False - if self.queue.full(): - event_bus.publish("speech.error", turn_id=turn_id, error="Speech queue full") - return False - chunks = queue.Queue(maxsize=256) - interrupt = self._interrupt - future = self.requests.submit(self._receive, text, chunks, interrupt, turn_id) - self._futures.add(future) - def finished(completed): - with self._submit_lock: self._futures.discard(completed) - # A done callback can run inline; register it outside the submit lock. - self.queue.put_nowait((text, turn_id, chunks, future, interrupt, start_offset, end_offset)) + full = self.queue.full() + if not full: + chunks = queue.Queue(maxsize=256) + interrupt = self._interrupt + future = self.requests.submit(self._receive, text, chunks, interrupt, turn_id) + self._futures.add(future) + def finished(completed): + with self._submit_lock: self._futures.discard(completed) + self.queue.put_nowait((text, turn_id, chunks, future, interrupt, start_offset, end_offset)) + # Publish and register callbacks outside the submit lock: both can run listeners inline. + if full: + event_bus.publish("speech.error", turn_id=turn_id, error="Speech queue full") + return False future.add_done_callback(finished) return True @@ -78,10 +80,11 @@ def submit_clip(self, path, *, volume=.65, max_seconds=3.0, context=None): clip = AudioClip(path, volume, max_seconds, dict(context or {})) with self._submit_lock: if self.closed.is_set(): return False - if self.queue.full(): - event_bus.publish('wake.feedback.error', asset='audio', error='Audio queue full; wake cue skipped') - return False - self.queue.put_nowait((clip, None, None, None, self._interrupt, 0, None)) + full = self.queue.full() + if not full: self.queue.put_nowait((clip, None, None, None, self._interrupt, 0, None)) + if full: + event_bus.publish('wake.feedback.error', asset='audio', error='Audio queue full; wake cue skipped') + return False event_bus.publish('wake.feedback.queued', **clip.context) return True diff --git a/Code/process/app_core/audio/wake_word.py b/Code/process/app_core/audio/wake_word.py index c884509..ceee631 100644 --- a/Code/process/app_core/audio/wake_word.py +++ b/Code/process/app_core/audio/wake_word.py @@ -8,6 +8,7 @@ import time from ..events.bus import event_bus +from ..events.outbox import Outbox from .wake_capture import WakeCapture, prepare_audio @@ -33,6 +34,7 @@ def __init__(self, config): raise ValueError("Invalid wake threshold or follow-up duration") self.directory = config.root / "persistent_memories" / "wake_words" self.lock = threading.RLock() + self.outbox = Outbox(event_bus) self.worker = DaemonExecutor(max_workers=1, thread_name_prefix="wake-detector", max_pending=2) self.model = None self.embeddings = [] @@ -211,6 +213,11 @@ def cancel_calibration(self): self.publish() def feed(self, frame, speaking=False): + try: + self._feed(frame, speaking) + finally: self.outbox.flush() + + def _feed(self, frame, speaking): with self.lock: if self.closed: return if self.recording is not None: @@ -218,8 +225,8 @@ def feed(self, frame, speaking=False): if outcome == 'timeout': self.recording = None self.error = 'Five-second limit reached; sample discarded. Click Record and try again.' - event_bus.publish('voice.calibration_discarded', error=self.error) - self.publish() + self.outbox.put('voice.calibration_discarded', error=self.error) + self.outbox.defer(self.publish) elif outcome == 'complete': self.recording = None self.job = self.worker.submit(self._enroll, pcm) diff --git a/Code/process/app_core/emotion/probe.py b/Code/process/app_core/emotion/probe.py index 224b48a..e74a946 100644 --- a/Code/process/app_core/emotion/probe.py +++ b/Code/process/app_core/emotion/probe.py @@ -275,14 +275,15 @@ def publish_teacher(self, event, publish): if event.stream == 'assistant': if self.active_group is not None and event.state.turn_id != self.active_group: return if self.prediction_turn_id == event.state.turn_id: return - publish(event.state) + # Callbacks reach DesktopState and the session; never run them under the condition. + publish(event.state) def _publish(self, state, group, cancelled, *, fallback=False): with self.condition: if self.closed or cancelled() or self.active_group != group: return self.prediction_turn_id = None if fallback or state is None else group callback = self.on_fallback if fallback else self.on_prediction - if state is not None and callback: callback(replace(state, turn_id=group)) + if state is not None and callback: callback(replace(state, turn_id=group)) def _restore(self): source = self.data_path if self.data_path.exists() else self.path if self.path.exists() else self.legacy_path diff --git a/Code/process/app_core/events/bus.py b/Code/process/app_core/events/bus.py index 71cdaca..99b3bb9 100644 --- a/Code/process/app_core/events/bus.py +++ b/Code/process/app_core/events/bus.py @@ -1,11 +1,14 @@ from __future__ import annotations +import logging import threading import time import uuid from dataclasses import asdict, dataclass, field from typing import Any, Callable +logger = logging.getLogger(__name__) + @dataclass class RuntimeEvent: @@ -21,6 +24,8 @@ def as_dict(self): return asdict(self) class EventBus: + """Synchronous: listeners run on the publisher's thread. Never publish while holding a + lock other than SessionManager._voice_lock; queue through events.outbox.Outbox instead.""" def __init__(self): self._lock = threading.RLock() self._listeners = [] @@ -41,7 +46,7 @@ def publish(self, event_type: str, *, turn_id=None, action_id=None, **payload): with self._lock: listeners = tuple(self._listeners) for listener in listeners: try: listener(event) - except Exception: pass + except Exception: logger.exception('Event listener %r failed on %s', listener, event_type) return event diff --git a/Code/process/app_core/events/outbox.py b/Code/process/app_core/events/outbox.py new file mode 100644 index 0000000..0dea160 --- /dev/null +++ b/Code/process/app_core/events/outbox.py @@ -0,0 +1,35 @@ +"""Ordered delivery of events and callbacks without holding the caller's locks. + +Bus listeners run on the publisher's thread and may take SessionManager._voice_lock, +so publishing while holding any other lock can deadlock. Queue work with put()/defer() +while holding your lock, then call flush() after releasing it. One thread drains at a +time, so items keep the order in which they were queued. +""" +from collections import deque +import threading + + +class Outbox: + def __init__(self, bus): + self.bus = bus + self._pending = deque() + self._lock = threading.Lock() + self._draining = False + + def put(self, event_type, **payload): + self._pending.append((self.bus.publish, (event_type,), payload)) + + def defer(self, function, *args, **kwargs): + self._pending.append((function, args, kwargs)) + + def flush(self): + while True: + with self._lock: + if self._draining or not self._pending: return + self._draining = True + try: + while self._pending: + function, args, kwargs = self._pending.popleft() + function(*args, **kwargs) + finally: + with self._lock: self._draining = False diff --git a/Code/process/app_core/events/resources.py b/Code/process/app_core/events/resources.py index 62d6d6c..709e1fc 100644 --- a/Code/process/app_core/events/resources.py +++ b/Code/process/app_core/events/resources.py @@ -2,12 +2,16 @@ import json import threading +from .outbox import Outbox + class ResourceEvents: def __init__(self, bus, getters): self.bus, self.getters = bus, getters self.lock = threading.RLock() self.previous = {} + self.generation = {} + self.outbox = Outbox(bus) self.unsubscribe = bus.subscribe(self.observe) def snapshot(self): @@ -18,13 +22,18 @@ def snapshot(self): return result def emit(self, topic): + # Getters may take session locks, so they run unlocked. A newer emit of the same + # topic supersedes an older one still computing, and the outbox publishes in queue + # order after the lock is released, so subscribers never see a stale value last. + with self.lock: generation = self.generation[topic] = self.generation.get(topic, 0) + 1 try: value = self.getters[topic]() except Exception: return key = json.dumps(value, sort_keys=True, default=str) with self.lock: - if self.previous.get(topic) == key: return - self.previous[topic] = key - self.bus.publish('resource.' + topic, **value) + if generation == self.generation[topic] and self.previous.get(topic) != key: + self.previous[topic] = key + self.outbox.put('resource.' + topic, **value) + self.outbox.flush() def observe(self, event): kind = event.type diff --git a/Code/process/app_core/integrations/discord/launcher.py b/Code/process/app_core/integrations/discord/launcher.py index c4e9767..34a18d7 100644 --- a/Code/process/app_core/integrations/discord/launcher.py +++ b/Code/process/app_core/integrations/discord/launcher.py @@ -9,12 +9,14 @@ from .access import DiscordAccess from ...events.bus import event_bus +from ...events.outbox import Outbox class DiscordLauncher: def __init__(self, root, state=None): self.root = Path(root) self.lock = threading.RLock() + self.outbox = Outbox(event_bus) # status/notifications reach the session; never emit under self.lock self.process = None self.error = '' self.access = DiscordAccess(root) @@ -65,6 +67,10 @@ def inbox_snapshot(self): with self.lock: return {'messages': [dict(item) for item in self.inbox], 'received': self.received, 'limit': 256} def report(self, client_id, value): + try: self._report(client_id, value) + finally: self.outbox.flush() + + def _report(self, client_id, value): if not isinstance(value, dict): raise ValueError('Invalid Discord report') with self.lock: if self.client_id != client_id: raise ValueError('Discord connection changed') @@ -72,13 +78,13 @@ def report(self, client_id, value): first = not self.ready self.ready, self.error = True, '' self.bot_name = str(value.get('bot_name', 'Discord bot'))[:100] - if first: self.notify('Discord connected as ' + self.bot_name + '.') - self.publish() + if first: self.outbox.defer(self.notify, 'Discord connected as ' + self.bot_name + '.') + self.outbox.defer(self.publish) return if value.get('kind') == 'disconnected': self.ready = False - self.notify('Discord gateway disconnected; reconnecting.', 'warning') - self.publish() + self.outbox.defer(self.notify, 'Discord gateway disconnected; reconnecting.', 'warning') + self.outbox.defer(self.publish) return if value.get('kind') != 'message': raise ValueError('Unknown Discord report') def snowflake(key, optional=False): @@ -112,9 +118,13 @@ def configure(self, values, revision): return result def start(self): + try: return self._start() + finally: self.outbox.flush() + + def _start(self): with self.lock: if self.status()['running']: - self.notify('Discord is already started' + (' and connected.' if self.ready else '; it is connecting.')) + self.outbox.defer(self.notify, 'Discord is already started' + (' and connected.' if self.ready else '; it is connecting.')) return {**self.status(), 'already_started': True} try: try: settings = self.access.settings() @@ -129,13 +139,13 @@ def start(self): creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) self.error = '' except ValueError as exc: - self.error = str(exc); self.publish(); raise + self.error = str(exc); self.outbox.defer(self.publish); raise except OSError as exc: - self.error = 'Discord client could not be started'; self.publish(); raise RuntimeError(self.error) from exc + self.error = 'Discord client could not be started'; self.outbox.defer(self.publish); raise RuntimeError(self.error) from exc process = self.process threading.Thread(target=self._watch, args=(process,), name='discord-process', daemon=True).start() - self.publish() - self.notify('Discord client is starting; waiting for its gateway connection.') + self.outbox.defer(self.publish) + self.outbox.defer(self.notify, 'Discord client is starting; waiting for its gateway connection.') return self.status() def _watch(self, process): diff --git a/Code/process/app_core/runtime/actions.py b/Code/process/app_core/runtime/actions.py index e15e237..86f95a1 100644 --- a/Code/process/app_core/runtime/actions.py +++ b/Code/process/app_core/runtime/actions.py @@ -13,6 +13,7 @@ from typing import Any from ..events.bus import event_bus +from ..events.outbox import Outbox @dataclass @@ -34,6 +35,7 @@ def __init__(self): self._lock = threading.RLock() self._actions: dict[str, CompanionAction] = {} self._closed = False + self._outbox = Outbox(event_bus) def start(self, kind: str, payload: dict[str, Any] | None = None, duration: float = 0.0) -> CompanionAction: duration = float(duration) @@ -42,20 +44,22 @@ def start(self, kind: str, payload: dict[str, Any] | None = None, duration: floa if not kind.strip(): raise ValueError("Action kind is required") action = CompanionAction(kind, deepcopy(payload or {}), duration) - with self._lock: - if self._closed: - raise RuntimeError("Action controller is closed") - # Each kind owns one presentation lane; newer intent replaces old. - for previous in list(self._actions.values()): - if previous.kind == kind: - self.cancel(previous.id) - self._actions[action.id] = action - if action.duration: - action._timer = threading.Timer(action.duration, self.complete, args=(action.id,)) - action._timer.daemon = True - event_bus.publish("action.started", action_id=action.id, action=action.as_dict()) - if action._timer and action.status == "running": - action._timer.start() + try: + with self._lock: + if self._closed: + raise RuntimeError("Action controller is closed") + # Each kind owns one presentation lane; newer intent replaces old. + for previous in list(self._actions.values()): + if previous.kind == kind: + self._cancel_locked(previous.id) + self._actions[action.id] = action + if action.duration: + action._timer = threading.Timer(action.duration, self.complete, args=(action.id,)) + action._timer.daemon = True + self._outbox.put("action.started", action_id=action.id, action=action.as_dict()) + if action._timer and action.status == "running": + action._timer.start() + finally: self._outbox.flush() return action def complete(self, action_id: str): @@ -65,8 +69,9 @@ def complete(self, action_id: str): action.status = "complete" if action._timer: action._timer.cancel() del self._actions[action_id] - event_bus.publish("action.completed", action_id=action.id, action=action.as_dict()) - return True + self._outbox.put("action.completed", action_id=action.id, action=action.as_dict()) + self._outbox.flush() + return True def update(self, action_id: str, payload: dict): """Change continuous targets without restarting the presentation lane.""" @@ -74,17 +79,22 @@ def update(self, action_id: str, payload: dict): action = self._actions.get(action_id) if not action or action.status != 'running': return False action.payload = deepcopy(payload) - event_bus.publish('action.updated', action_id=action.id, action=action.as_dict()) - return True + self._outbox.put('action.updated', action_id=action.id, action=action.as_dict()) + self._outbox.flush() + return True def cancel(self, action_id: str): - with self._lock: - action = self._actions.get(action_id) - if not action or action.status != "running": return False - action.status = "cancelled" - if action._timer: action._timer.cancel() - del self._actions[action_id] - event_bus.publish("action.cancelled", action_id=action.id, action=action.as_dict()) + with self._lock: cancelled = self._cancel_locked(action_id) + self._outbox.flush() + return cancelled + + def _cancel_locked(self, action_id): + action = self._actions.get(action_id) + if not action or action.status != "running": return False + action.status = "cancelled" + if action._timer: action._timer.cancel() + del self._actions[action_id] + self._outbox.put("action.cancelled", action_id=action.id, action=action.as_dict()) return True def set_emotion(self, state): @@ -106,4 +116,5 @@ def active(self): def close(self): with self._lock: self._closed = True - for action in list(self.active()): self.cancel(action["id"]) + for action_id in list(self._actions): self._cancel_locked(action_id) + self._outbox.flush() diff --git a/Code/process/app_core/runtime/initiative.py b/Code/process/app_core/runtime/initiative.py index 6308956..02a8fea 100644 --- a/Code/process/app_core/runtime/initiative.py +++ b/Code/process/app_core/runtime/initiative.py @@ -368,10 +368,10 @@ def _run(self): rule, event, queued = job self.pending.pop(rule['id'], None) self.busy = True - event_bus.publish('initiative.started') version = self.version else: job = None if job: + event_bus.publish('initiative.started') self.executor.submit(self._evaluate_job, rule, event, queued, version) except Exception as exc: with self.lock: self.error = str(exc) diff --git a/Code/process/app_core/runtime/session.py b/Code/process/app_core/runtime/session.py index 1a65429..2873abf 100644 --- a/Code/process/app_core/runtime/session.py +++ b/Code/process/app_core/runtime/session.py @@ -27,6 +27,9 @@ def __init__(self, config, chat, state, actions=None): if provider is not None: provider.expression_idle=lambda:not (self._generation_active or self._playing or self._speech_pending or self._user_speaking) self.actions = actions or ActionController() + # Lock order: _capture_lock (request handlers only) -> _voice_lock -> any component lock. + # Bus listeners take _voice_lock, so components must not publish while holding their own + # locks (use events.outbox.Outbox); tests/test_lock_discipline.py enforces this. self._turn_lock = threading.Lock() self._voice_lock = threading.RLock() self._capture_lock = threading.Lock() @@ -258,6 +261,8 @@ def _wake_feedback_event(self, event): self.wake_feedback.trigger(emotion.get('primary', 'neutral'), model_state, audio_enabled=audio_enabled) def _playback_event(self, event): + # Only voice/speech events change playback state; skip the rest before locking. + if not event.type.startswith(('voice.', 'speech.')): return with self._voice_lock: if event.type == 'voice.starting': self._voice_phase = 'starting' elif event.type == 'voice.activated': self._voice_phase = 'awake' diff --git a/tests/test_lock_discipline.py b/tests/test_lock_discipline.py new file mode 100644 index 0000000..6e18f20 --- /dev/null +++ b/tests/test_lock_discipline.py @@ -0,0 +1,166 @@ +"""Bus listeners run on the publisher's thread and take SessionManager._voice_lock, so +publishing (or running callbacks) while holding any other lock creates lock-order deadlocks.""" +import ast +from pathlib import Path +import threading +from types import SimpleNamespace + +from process.app_core.desktop.state import DesktopState +from process.app_core.events.bus import EventBus, event_bus +from process.app_core.events.outbox import Outbox +from process.app_core.events.resources import ResourceEvents +from process.app_core.runtime.actions import ActionController + +ROOT = Path(__file__).resolve().parents[1] / 'Code' +EMITTERS = {'publish', 'emit', '_emit', 'notify', 'on_prediction', 'on_fallback', 'callback'} +# _voice_lock is the outermost runtime lock; _capture_lock is taken only by request handlers above it. +ALLOWED = {('process/app_core/runtime/session.py', 'self._voice_lock'), ('process/app_core/runtime/session.py', 'self._capture_lock')} + + +def emitting_calls(node): + """Calls in a with-body, skipping nested functions (they run later, without the lock).""" + for child in ast.iter_child_nodes(node): + if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)): continue + if isinstance(child, ast.Call): + function = child.func + name = function.attr if isinstance(function, ast.Attribute) else getattr(function, 'id', '') + receiver = ast.unparse(function.value) if isinstance(function, ast.Attribute) else '' + if name in EMITTERS and not (name == 'notify' and 'cond' in receiver.lower()): yield child + yield from emitting_calls(child) + + +def test_nothing_publishes_or_calls_back_while_holding_a_component_lock(): + violations = [] + for path in sorted(ROOT.rglob('*.py')): + relative = path.relative_to(ROOT).as_posix() + for node in ast.walk(ast.parse(path.read_text(encoding='utf-8'))): + if not isinstance(node, ast.With): continue + for item in node.items: + lock = ast.unparse(item.context_expr) + if not any(word in lock.lower() for word in ('lock', 'condition')) or (relative, lock) in ALLOWED: continue + violations += [f'{relative}:{call.lineno} {ast.unparse(call)[:80]} (under {lock})' for call in emitting_calls(node)] + assert not violations, 'Publish after releasing the lock (see events/outbox.py):\n' + '\n'.join(violations) + + +def run_both(first, second, timeout=5): + threads = [threading.Thread(target=first, daemon=True), threading.Thread(target=second, daemon=True)] + for thread in threads: thread.start() + for thread in threads: thread.join(timeout) + return [thread.is_alive() for thread in threads] + + +def test_wake_calibration_timeout_does_not_deadlock_with_runtime_snapshot(monkeypatch): + """Regression: WakeWord.feed published under WakeWord.lock while runtime_snapshot held _voice_lock.""" + import process.app_core.runtime.session as session_module + from process.app_core.audio.wake_capture import WakeCapture + + class FakeSpeech: + def __init__(self, *args): pass + def submit(self, *args): return False + def submit_clip(self, *args, **kwargs): return False + def cancel(self): pass + def close(self): pass + + in_publish, voice_held = threading.Event(), threading.Event() + def gate(event): + if event.type == 'voice.calibration_discarded' and threading.current_thread().name == 'vad': + in_publish.set() + voice_held.wait(2) + unsubscribe = event_bus.subscribe(gate) + monkeypatch.setattr(session_module, 'SpeechQueue', FakeSpeech) + chat = SimpleNamespace(history=[], _save_history=lambda: None, provider=SimpleNamespace(close=lambda: None)) + config = SimpleNamespace(raw={}, root=Path('.'), character_name='Riko', tools=SimpleNamespace(max_iterations=8)) + session = session_module.SessionManager(config, chat, DesktopState()) + session.wake.recording = WakeCapture(max_seconds=0.01) # next frame times out the sample + def vad(): + threading.current_thread().name = 'vad' + session.wake.feed(bytes(1024), False) + def http(): # what runtime_snapshot does: _voice_lock, then wake.status() + in_publish.wait(2) + with session._voice_lock: + voice_held.set() + session.wake.status() + try: assert run_both(vad, http) == [False, False] + finally: + unsubscribe() + try: session.close() + except Exception: pass + + +def test_action_events_do_not_deadlock_with_a_caller_holding_the_voice_lock(): + """Regression: ActionController published under its lock while barge-in held _voice_lock.""" + bus_listener_started, voice_held = threading.Event(), threading.Event() + voice_lock = threading.RLock() + actions = ActionController() + def listener(event): # like SessionManager._playback_event / desktop snapshots + if event.type == 'action.started' and threading.current_thread().name == 'animation': + bus_listener_started.set() + voice_held.wait(2) + with voice_lock: pass + unsubscribe = event_bus.subscribe(listener) + def animation(): + threading.current_thread().name = 'animation' + actions.start('motion', {'name': 'idle'}) + def barge_in(): # SessionManager.cancel under _voice_lock asks the controller for active actions + bus_listener_started.wait(2) + with voice_lock: + voice_held.set() + actions.active() + try: assert run_both(animation, barge_in) == [False, False] + finally: + unsubscribe() + actions.close() + + +def test_outbox_delivers_in_queue_order_even_when_another_thread_is_draining(): + bus, seen = EventBus(), [] + outbox = Outbox(bus) + release = threading.Event() + def slow(event): + seen.append(event.payload['n']) + if event.payload['n'] == 0: release.wait(2) + bus.subscribe(slow) + outbox.put('step', n=0) + drainer = threading.Thread(target=outbox.flush, daemon=True) + drainer.start() + outbox.put('step', n=1) + outbox.flush() # returns at once: the drainer owns delivery and will publish n=1 next + release.set() + drainer.join(2) + assert seen == [0, 1] + + +def test_actions_publish_started_then_cancelled_in_order(): + seen = [] + unsubscribe = event_bus.subscribe(lambda event: seen.append(event.type) if event.type.startswith('action.') else None) + actions = ActionController() + try: + first = actions.start('gesture', {'name': 'nod'}) + actions.start('gesture', {'name': 'wave'}) # same lane: cancels the first + actions.cancel(first.id) # already cancelled: no event + actions.close() # cancels the second + finally: unsubscribe() + assert seen == ['action.started', 'action.cancelled', 'action.started', 'action.cancelled'] + + +def test_resource_emit_never_publishes_a_superseded_value_last(): + bus, published = EventBus(), [] + bus.subscribe(lambda event: published.append(event.payload['value']) if event.type == 'resource.voice' else None) + first_computing, newer_done = threading.Event(), threading.Event() + values = iter(['old', 'new']) + def getter(): + value = next(values) + if value == 'old': + first_computing.set() + newer_done.wait(2) # the older computation finishes after the newer one + return {'value': value} + bridge = ResourceEvents(bus, {'voice': getter}) + try: + older = threading.Thread(target=bridge.emit, args=('voice',), daemon=True) + older.start() + first_computing.wait(2) + bridge.emit('voice') + newer_done.set() + older.join(2) + finally: bridge.close() + assert published == ['new'] From 23febe5809d2f83c39c58fa60128d8e88b25dbc6 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 00:51:50 +0530 Subject: [PATCH 04/37] Keep user data on Stop, failures and unreadable stores; free the event loop - cancel() only cuts a turn still generating or playing: Stop, Sleep and shutdown no longer erase a finished reply; muted or failed TTS keeps the visible text. - Failed, cancelled and shutdown-interrupted turns keep the user's message; errors are archived as errors, not interruptions. - respond() releases the turn lock if superseding the previous reply fails, and validates voice settings before changing turn state. - Unreadable chat history, tool approvals, todo list and probe files are kept as .unreadable- (persistence/preserve.py) or never saved over. Corrupt approvals require approval for every tool; the panel shows the error. - Probe: weights-only restore from probe.pt; weights not loaded are kept on disk. - todo_list: stable IDs from a persisted counter, never reused. - WS bootstrap snapshots, /api/chat, whiteboard capture and Discord attach/detach no longer take session locks on the event loop; close() unsubscribes first. - Log listener/getter failures that were swallowed. Fixes audit findings V001 V012 V015 V018 V067 V068 V114 V115 V160, plus the issues confirmed by the adversarial review of this change. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 +- Code/desktop_server.py | 18 +- Code/process/app_core/audio/speech.py | 2 +- Code/process/app_core/conversation/chat.py | 25 +- Code/process/app_core/desktop/state.py | 2 +- Code/process/app_core/emotion/probe.py | 104 ++++-- Code/process/app_core/events/resources.py | 13 +- Code/process/app_core/events/stream.py | 3 +- .../persistence/conversation_store.py | 3 +- Code/process/app_core/persistence/preserve.py | 19 + Code/process/app_core/runtime/session.py | 108 ++++-- Code/process/app_core/tools/approval.py | 33 +- .../app_core/tools/builtin/todo_list.py | 90 +++-- electron/src/tool_approvals.jsx | 2 +- tests/test_data_integrity.py | 350 ++++++++++++++++++ 15 files changed, 664 insertions(+), 112 deletions(-) create mode 100644 Code/process/app_core/persistence/preserve.py create mode 100644 tests/test_data_integrity.py diff --git a/CLAUDE.md b/CLAUDE.md index d871737..44a2f8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. ### Persistence The data root is the directory holding `character_config.yaml`. - `persistent_memories/` is user data: never delete it during cleanup. Runtime files: - - `chat_history.json`: the model's context. Both `ChatService` and `SessionManager` rewrite the whole file, and an invalid file is treated as empty. + - `chat_history.json`: the model's context. Both `ChatService` and `SessionManager` rewrite the whole file (fsynced). An unreadable file is kept as `chat_history.json.unreadable-` before anything replaces it. Only a turn still generating or playing is cut by Stop/Sleep/shutdown; failed and cancelled turns keep the user's message. - `memory_store.json` (`app_core/persistence/memory.py`): written via temp file, fsync and replace; fails closed. Its FAISS index is written but rebuilt from embeddings on load. - `conversations.sqlite3`: the UI archive, built from bus events. It is a second history and can diverge from `chat_history.json`. - `tasks.sqlite3` (`app_core/persistence/tasks.py`): revision-checked, and shared with `Code/task_mcp_server.py`. @@ -146,7 +146,7 @@ The data root is the directory holding `character_config.yaml`. - `runtime.native_library: bundled:` resolves to `$RIKO_BUNDLE_ROOT/native//`. ## Conventions and gotchas -- Writes go to a temp file and then `os.replace`; the memory store and settings also fsync. Stores fail closed rather than resetting user data. +- Writes go to a temp file and then `os.replace`; the memory store, settings and chat history also fsync. Stores fail closed rather than resetting user data: keep an unreadable file with `persistence/preserve.py` (`.unreadable-`), or refuse to save over it. - Optimistic concurrency uses revisions throughout: the settings `revision`, task `expected_revision` (`TaskConflict` → 409), the Discord access revision, memory-record revisions, and whiteboard `board_revision`. - File paths supplied by the renderer or the model must go through `resolve_media` (`app_core/desktop/media.py`: approved roots plus an extension allowlist). - Heavy dependencies (torch, faster_whisper, sounddevice, silero_vad, sentence_transformers, openai, huggingface_hub) are imported lazily, and tests swap them through `sys.modules`. Services accept `start=False` / `start_worker=False`. Tests must close services and unsubscribe from the global `event_bus` in `finally`. diff --git a/Code/desktop_server.py b/Code/desktop_server.py index a107e13..f79cd60 100644 --- a/Code/desktop_server.py +++ b/Code/desktop_server.py @@ -1,6 +1,7 @@ """Local HTTP/WebSocket bridge for the Electron desktop client.""" from __future__ import annotations +import asyncio import json import logging from contextlib import asynccontextmanager @@ -246,14 +247,17 @@ def replay(): @app.websocket('/ws/discord/client') async def discord_client(websocket: WebSocket, instance: str): - try: discord_launcher.attach(instance) + # attach/detach publish state that reaches session locks: run them off the event loop. + try: await run_in_threadpool(discord_launcher.attach, instance) except (ValueError, TypeError): await websocket.accept(); await websocket.close(code=1008, reason='A Discord client is already connected or the instance ID is invalid'); return async def report(value): await run_in_threadpool(discord_launcher.report, instance, value) try: await stream_events(websocket, event_bus, snapshot, initial=lambda: resource_events.snapshot() if resource_events else {'discord': discord_launcher.status()}, on_message=report) - finally: discord_launcher.detach(instance) + finally: + # Shielded: detach must still free the client slot if this handler is being cancelled. + await asyncio.shield(run_in_threadpool(discord_launcher.detach, instance)) @app.get('/api/resources/gpu') def gpu_status(): @@ -274,6 +278,7 @@ def approval_policy(request: dict): registry = approval_registry() try: return registry.approvals.configure(request.get('policy'), registry.tools) except ValueError as exc: raise HTTPException(400, str(exc)) + except OSError as exc: raise HTTPException(409, f'Could not save tool permissions: {exc}') @app.post('/api/tools/approvals/{request_id}') def resolve_approval(request_id: str, request: dict): @@ -450,7 +455,8 @@ async def whiteboard_capture(request: Request, revision: str): async for chunk in request.stream(): if len(png) + len(chunk) > 8 * 1024 * 1024: raise HTTPException(413, 'Board image exceeds 8 MiB') png.extend(chunk) - try: accepted = board_images.capture(revision, bytes(png), state.snapshot(), event_bus) + # PIL verification, hashing and the publish run off the event loop. + try: accepted = await run_in_threadpool(lambda: board_images.capture(revision, bytes(png), state.snapshot(), event_bus)) except (ValueError, OSError) as exc: raise HTTPException(400, 'Invalid board image') from exc return {'accepted': accepted} @@ -722,9 +728,11 @@ def delete_memory(record_id: str): return {"deleted": record_id} @app.post("/api/chat") -async def chat_endpoint(request: ChatRequest): +def chat_endpoint(request: ChatRequest): + # Plain def: FastAPI runs it in the thread pool, so the state/snapshot calls below + # (which take session locks) never block the event loop. try: - response = await run_in_threadpool(session.respond, request.text, request.user_name) + response = session.respond(request.text, request.user_name) except TurnCancelled: return {"cancelled": True} state.set_speech(response.message.content) diff --git a/Code/process/app_core/audio/speech.py b/Code/process/app_core/audio/speech.py index 9473271..6221677 100644 --- a/Code/process/app_core/audio/speech.py +++ b/Code/process/app_core/audio/speech.py @@ -58,7 +58,7 @@ def finished(completed): self.queue.put_nowait((text, turn_id, chunks, future, interrupt, start_offset, end_offset)) # Publish and register callbacks outside the submit lock: both can run listeners inline. if full: - event_bus.publish("speech.error", turn_id=turn_id, error="Speech queue full") + event_bus.publish("speech.error", turn_id=turn_id, error="Speech queue full", queued=False) return False future.add_done_callback(finished) return True diff --git a/Code/process/app_core/conversation/chat.py b/Code/process/app_core/conversation/chat.py index 361f974..069bc7c 100644 --- a/Code/process/app_core/conversation/chat.py +++ b/Code/process/app_core/conversation/chat.py @@ -2,12 +2,14 @@ import json import logging +import os import uuid from pathlib import Path from typing import Callable from .messages import ChatMessage, ModelResponse, conversation_sections from .output_filter import OutputFilter, clean_output +from ..persistence.preserve import preserve_unreadable logger = logging.getLogger(__name__) @@ -42,6 +44,7 @@ def observe_output_delta(self, delta: str, *, final: bool = False): return self.emotion_engine.observe_output(delta, final=final) if self.emotion_engine else None def _load_history(self): + self._unreadable_history = False if not self.history_file or not self.history_file.exists(): return [] try: raw = json.loads(self.history_file.read_text(encoding="utf-8")) @@ -50,15 +53,31 @@ def _load_history(self): return [ChatMessage(x["role"], x.get("content", ""), tool_call_id=x.get("tool_call_id"), timestamp=x.get('timestamp'), source=x.get('source') if x.get('source') in {'discord','microphone','message'} else None, conversation_id=str(x['conversation_id'])[:200] if x.get('conversation_id') else None) for x in raw if x.get("role") != "system"] - except (OSError, ValueError, KeyError): - logger.warning("Unable to load chat history; starting with an empty history") + except (OSError, ValueError, KeyError) as exc: + logger.warning("Unable to load chat history (%s); starting with an empty history", exc) + self._unreadable_history = True + self._preserve_unreadable_history() return [] + def _preserve_unreadable_history(self): + """Keep an unreadable history aside before any save replaces it. Fails closed.""" + try: backup = preserve_unreadable(self.history_file) + except OSError as exc: + logger.error('Could not back up unreadable %s (%s); chat history is not saved over it', self.history_file.name, exc) + return False + if backup: logger.warning('Kept the unreadable chat history as %s', backup.name) + self._unreadable_history = False + return True + def _save_history(self): if not self.history_file: return + if self._unreadable_history and not self._preserve_unreadable_history(): return self.history_file.parent.mkdir(parents=True, exist_ok=True) temporary = self.history_file.with_suffix(self.history_file.suffix + '.tmp') - temporary.write_text(json.dumps([m.as_record() for m in self.history], indent=2), encoding="utf-8") + with temporary.open('w', encoding='utf-8') as stream: + stream.write(json.dumps([m.as_record() for m in self.history], indent=2)) + stream.flush() + os.fsync(stream.fileno()) temporary.replace(self.history_file) def respond(self, text: str, user_name: str = "User", *, max_iterations: int = 8, on_delta=None, on_reasoning=None, on_metrics=None, cancelled=lambda: False, response_history=None, record_user=True) -> ModelResponse: diff --git a/Code/process/app_core/desktop/state.py b/Code/process/app_core/desktop/state.py index 13084a9..542ee73 100644 --- a/Code/process/app_core/desktop/state.py +++ b/Code/process/app_core/desktop/state.py @@ -69,7 +69,7 @@ def _emit(self, event, value=None): self._save_board() for listener in tuple(self._listeners): try: listener(event, value) - except Exception: pass + except Exception: logging.getLogger(__name__).exception('Desktop state listener %r failed on %s', listener, event) def snapshot(self): with self._lock: diff --git a/Code/process/app_core/emotion/probe.py b/Code/process/app_core/emotion/probe.py index e74a946..86a4d73 100644 --- a/Code/process/app_core/emotion/probe.py +++ b/Code/process/app_core/emotion/probe.py @@ -151,6 +151,8 @@ def __init__(self, directory, identity, teacher, config, *, idle=lambda: True, o self.metadata = {} self.save_lock = threading.Lock() self.error = '' + self.save_blocked = '' # Set when unreadable artifacts could not be kept aside: never overwrite them. + self._dormant = None # (weights, validation) restored but not loaded; written back until replaced. self.segment_features=deque(maxlen=512) self._restore() self.thread = threading.Thread(target=self._run, name='emotion-probe', daemon=True) @@ -227,7 +229,7 @@ def edit_sample(self, sample_id, values, revision): self.samples[index]=(row[0],target,row[2]);found=True if not found: raise ValueError('Sample is no longer available') self.metadata.setdefault(sample_id,{})['edited']=True - self.network=None;self.validation={};self.since_train=max(self.since_train,self.config.retrain_every) + self.network=None;self.validation={};self._dormant=None;self.since_train=max(self.since_train,self.config.retrain_every) self.data_revision=str(uuid.uuid4()) self._save(list(self.samples)) return self.status() @@ -285,27 +287,80 @@ def _publish(self, state, group, cancelled, *, fallback=False): callback = self.on_fallback if fallback else self.on_prediction if state is not None and callback: callback(replace(state, turn_id=group)) + def _load_artifact(self, path): + """Return a readable artifact dict, or None after setting the file aside (or blocking saves).""" + import torch + try: + saved = torch.load(path, map_location='cpu', weights_only=True) + if not isinstance(saved, dict): raise ValueError('Not an emotion probe artifact') + return saved + except Exception as exc: + self._quarantine(path, exc) + return None + + def _quarantine(self, path, exc): + if path == self.legacy_path: + # Legacy storage is read-only discovery: saves never write there, so leave it alone. + self.error = f'Ignored unreadable legacy probe file {path.name} ({exc})' + logger.warning('Ignoring unreadable legacy emotion probe file %s', path, exc_info=exc) + return + logger.warning('Unreadable emotion probe file %s', path, exc_info=exc) + related = [path, path.with_name('examples.json')] if path == self.data_path and self.data_path != self.path else [path] + if all(self._set_aside(item) for item in related): + self.error = f'Unreadable probe file kept as a backup: {path.name} ({exc})' + else: + self.error = self.save_blocked = f'Could not read or set aside {path.name}; probe data is not saved until it is readable' + + def _set_aside(self, path): + """Move an unreadable file out of the way so later saves cannot overwrite it.""" + if not path.exists(): return True + target = path.with_name(f'{path.name}.unreadable-{time.strftime("%Y%m%d-%H%M%S")}-{uuid.uuid4().hex[:6]}') + try: path.replace(target) + except OSError: + logger.exception('Could not set aside %s', path) + return False + logger.warning('Kept unreadable emotion probe file as %s', target) + return True + def _restore(self): - source = self.data_path if self.data_path.exists() else self.path if self.path.exists() else self.legacy_path - if source is None or not source.exists(): return + # Fail closed: unreadable files are set aside or block saving, never silently overwritten. + source = next((path for path in (self.data_path, self.path, self.legacy_path) if path is not None and path.exists()), None) + if source is None: return + def fall_back(): + # training.pt was set aside, but the trained weights in probe.pt may still be fine. + if source == self.data_path and self.data_path != self.path and not self.save_blocked: self._restore() + saved = self._load_artifact(source) + if saved is None: return fall_back() + if saved.get('identity') != self.identity: return + weights, weights_validation = saved.get('weights'), saved.get('validation') + if source == self.data_path and self.data_path != self.path and self.path.exists(): + artifact = self._load_artifact(self.path) + if artifact is not None and artifact.get('identity') == self.identity: + weights, weights_validation = artifact.get('weights'), artifact.get('validation') try: - import torch - saved = torch.load(source, map_location='cpu', weights_only=True) - if saved['identity'] != self.identity: return - self.samples.extend(saved['samples'][-self.config.max_samples:]) - self.since_train = min(len(self.samples), saved.get('since_train', len(self.samples))) - self.validation = saved['validation'] - self.metadata = saved.get('metadata', {}) - if source == self.data_path and self.data_path != self.path and self.path.exists(): - artifact = torch.load(self.path, map_location='cpu', weights_only=True) - if artifact.get('identity') == self.identity: - saved['weights'] = artifact.get('weights') - if saved.get('weights') is not None and qualified(self.validation, self.config): + samples, since_train, metadata = [], 0, {} + if 'samples' in saved: # A weights-only probe.pt has no samples, metadata or since_train. + samples = list(saved['samples'])[-self.config.max_samples:] + since_train = min(len(samples), int(saved.get('since_train', len(samples)))) + metadata = dict(saved.get('metadata') or {}) + validation = dict(saved.get('validation') or {}) + except Exception as exc: + self._quarantine(source, exc) + return fall_back() + self.samples.extend(samples) + self.since_train, self.metadata, self.validation = since_train, metadata, validation + if weights is not None: + try: + weights_validation = dict(weights_validation or {}) + if not qualified(weights_validation, self.config): raise ValueError('below the configured quality thresholds') network = build_network(self.config) - network.load_state_dict(saved['weights']) - self.network = network.eval().requires_grad_(False) - except Exception: - logger.warning('Ignoring incompatible emotion probe artifact', exc_info=True) + network.load_state_dict(weights) + self.network, self.validation = network.eval().requires_grad_(False), weights_validation + except Exception as exc: + # Unused now (stricter thresholds or incompatible), but still the user's trained + # weights: keep writing them back so a later save cannot erase the only copy. + self._dormant = (weights, weights_validation) + logger.warning('Trained emotion probe weights not loaded (%s); keeping them on disk', exc) def _run(self): try: self._work() @@ -392,6 +447,7 @@ def _train(self): if qualified(result, self.config) and (incumbent is None or result['macro_f1'] >= incumbent['macro_f1']): self.network = network self.validation = result + self._dormant = None elif self.network is None: self.validation = result self.since_train = 0 self._save(data) @@ -400,18 +456,24 @@ def _train(self): def _save(self, samples): import torch + if self.save_blocked: + logger.warning('Emotion probe not saved: %s', self.save_blocked) + return self.path.parent.mkdir(parents=True, exist_ok=True) self.data_path.parent.mkdir(parents=True, exist_ok=True) temporary = self.path.with_suffix('.' + uuid.uuid4().hex + '.tmp') with self.save_lock: + weights = {key: value.cpu() for key, value in self.network.state_dict().items()} if self.network is not None else None + artifact_validation = self.validation + if weights is None and self._dormant: weights, artifact_validation = self._dormant dataset = {'identity': self.identity, 'samples': samples, 'validation': self.validation, 'since_train': self.since_train, - 'metadata':self.metadata, - 'weights': {key: value.cpu() for key, value in self.network.state_dict().items()} if self.network is not None else None} + 'metadata':self.metadata, 'weights': weights} if self.data_path != self.path: data_temporary = self.data_path.with_suffix('.' + uuid.uuid4().hex + '.tmp') torch.save({key: value for key, value in dataset.items() if key != 'weights'}, data_temporary) data_temporary.replace(self.data_path) artifact = {key: value for key, value in dataset.items() if key not in {'samples', 'metadata', 'since_train'}} + artifact['validation'] = artifact_validation else: artifact = dataset torch.save(artifact, temporary) diff --git a/Code/process/app_core/events/resources.py b/Code/process/app_core/events/resources.py index 709e1fc..360b7e0 100644 --- a/Code/process/app_core/events/resources.py +++ b/Code/process/app_core/events/resources.py @@ -1,9 +1,12 @@ """Snapshots originate at mutation events, never periodic status requests.""" import json +import logging import threading from .outbox import Outbox +logger = logging.getLogger(__name__) + class ResourceEvents: def __init__(self, bus, getters): @@ -18,7 +21,9 @@ def snapshot(self): result = {} for topic, getter in self.getters.items(): try: result[topic] = getter() - except Exception: result[topic] = None # Runtime may be starting/stopping. + except Exception: # Runtime may be starting/stopping. + logger.debug('Resource %s unavailable for snapshot', topic, exc_info=True) + result[topic] = None return result def emit(self, topic): @@ -27,7 +32,11 @@ def emit(self, topic): # order after the lock is released, so subscribers never see a stale value last. with self.lock: generation = self.generation[topic] = self.generation.get(topic, 0) + 1 try: value = self.getters[topic]() - except Exception: return + except Exception as exc: + # Getters are HTTP handlers: an HTTP error (status_code) means the feature is off. + level = logging.DEBUG if hasattr(exc, 'status_code') else logging.WARNING + logger.log(level, 'Resource %s could not be read; panels keep the previous value', topic, exc_info=True) + return key = json.dumps(value, sort_keys=True, default=str) with self.lock: if generation == self.generation[topic] and self.previous.get(topic) != key: diff --git a/Code/process/app_core/events/stream.py b/Code/process/app_core/events/stream.py index 00398c5..f90b6d6 100644 --- a/Code/process/app_core/events/stream.py +++ b/Code/process/app_core/events/stream.py @@ -28,7 +28,8 @@ def listener(event): except RuntimeError: pass async def send(): - await websocket.send_json({'type': 'state.snapshot', 'payload': snapshot()}) + # snapshot() takes session locks; never block the event loop (and every client) on them. + await websocket.send_json({'type': 'state.snapshot', 'payload': await asyncio.to_thread(snapshot)}) watermark = 0 if initial: watermark = bus.cursor() diff --git a/Code/process/app_core/persistence/conversation_store.py b/Code/process/app_core/persistence/conversation_store.py index 70c21ec..e55e963 100644 --- a/Code/process/app_core/persistence/conversation_store.py +++ b/Code/process/app_core/persistence/conversation_store.py @@ -81,7 +81,8 @@ def observe(self, event): if event.type == 'chat.delta': data['text'] += p.get('text', '') elif event.type in {'chat.input', 'chat.completed'}: data.update(text=p.get('text', ''), status='completed') elif event.type == 'chat.interrupted': data.update(interrupted=True, cutoff=p.get('offset'), text=p.get('text', data['text'])) - elif event.type in {'chat.cancelled', 'model.error'}: data.update(interrupted=True, status='cancelled' if event.type == 'chat.cancelled' else 'error', error=p.get('error','')) + elif event.type == 'chat.cancelled': data.update(interrupted=True, status='cancelled', error=p.get('error','')) + elif event.type == 'model.error': data.update(status='error', error=p.get('error','')) # A failure is not an interruption. elif event.type == 'chat.interjection': items = data.setdefault('interjections', []) last = items[-1] if items else None diff --git a/Code/process/app_core/persistence/preserve.py b/Code/process/app_core/persistence/preserve.py new file mode 100644 index 0000000..ba179fa --- /dev/null +++ b/Code/process/app_core/persistence/preserve.py @@ -0,0 +1,19 @@ +"""Keep the bytes of an unreadable user file before anything replaces it.""" +from datetime import datetime +import shutil + + +def preserve_unreadable(path): + """Copy path to .unreadable-, or move it there when it cannot be read. + + Moving needs only directory access, so a permission-denied file can still be kept. + Returns the backup path, or None if the file no longer exists. Raises OSError when + the file can be neither copied nor moved; callers must then not overwrite it. + """ + backup = path.with_name(f'{path.name}.unreadable-{datetime.now():%Y%m%d-%H%M%S-%f}') + try: shutil.copy2(path, backup) + except FileNotFoundError: return None + except OSError: + try: path.replace(backup) + except FileNotFoundError: return None + return backup diff --git a/Code/process/app_core/runtime/session.py b/Code/process/app_core/runtime/session.py index 2873abf..36538d3 100644 --- a/Code/process/app_core/runtime/session.py +++ b/Code/process/app_core/runtime/session.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import re import threading import time @@ -17,6 +18,8 @@ from .lifecycle import close_bounded from ..audio.wake_feedback import WakeFeedback +logger = logging.getLogger(__name__) + class SessionManager: """Own turns independently of capture, transcription and ordered playback.""" @@ -55,6 +58,7 @@ def __init__(self, config, chat, state, actions=None): self._interrupt_notified = False self._generation_active = False self._speech_pending = 0 + self._speech_failed = False # TTS failed this turn: the user read the text instead of hearing it self._unsubscribe_speech = event_bus.subscribe(self._playback_event) self.voice = None self._voice_status = 'stopped' @@ -198,6 +202,7 @@ def present_initiative(self, message, *, spoken=False, guard=lambda: False): self._interrupt_notified = False self._generated = message self._spoken_offset = self._speech_cursor = 0 + self._speech_failed = False self._playing = self._cutoff = None self._assertive_until = 0 self._assertive_used = False @@ -291,7 +296,10 @@ def _playback_event(self, event): self._live_transcript = {**event.payload, 'text': ' '.join(event.payload.get('text', '').split()[:400])} if event.payload.get('final'): self._voice_phase = 'awake' if self.wake.active() else 'waiting' if event.turn_id != self._active_turn: return - if event.type in {"speech.completed", "speech.cancelled", "speech.error"}: + # A chunk rejected at submit (queued=False, e.g. queue full) was never pending and does not + # mean synthesis failed; later chunks still play. Only real TTS/playback errors count. + if event.type == "speech.error" and event.payload.get('queued', True): self._speech_failed = True + if event.type in {"speech.completed", "speech.cancelled", "speech.error"} and event.payload.get('queued', True): self._speech_pending = max(0, self._speech_pending - 1) if event.type == "speech.started" and not self._interrupt_notified: self._playing = dict(event.payload) @@ -368,11 +376,14 @@ def voice_transcript(self, text, started_at, ended_at, anchor=None): self._input_text += '\n' + addition self._input_continued = True self._interaction_revision += 1 - self._rewrite_history() - event_bus.publish('chat.input', turn_id=self._input_turn_id, text=self._input_text, user_name=self._input_user_name, **getattr(self, '_origin', {})) # Refresh inference with the combined user input; reasoning is not # visible output and must not create a speaking-over annotation. if self._generation_active: self.cancel() + # Nothing was visible when the user resumed, so a reply cut for the refresh (now, or + # earlier by sustained speech) is discarded, never kept as read, even when muted. + if self._interrupt_notified: self._cutoff = 0 + self._rewrite_history() + event_bus.publish('chat.input', turn_id=self._input_turn_id, text=self._input_text, user_name=self._input_user_name, **getattr(self, '_origin', {})) return True offset = anchor[1] if anchor else self._audible_offset() # On a sustained interruption, keep the user turn after the portion @@ -398,13 +409,22 @@ def respond(self, text: str, user_name="User", *, record_user=True, speak=True, self._interaction_revision += 1 if not self._turn_lock.acquire(blocking=False): raise RuntimeError("Riko is already handling another turn") - if self._playing or self._speech_pending: - self.cancel() # Foreground input supersedes queued initiative/previous speech. - base = len(self.chat.history) - from ..tools.approval import approval_turn - approval_token = approval_turn.set(turn_id) + try: + if self._playing or self._speech_pending: + self.cancel() # Foreground input supersedes queued initiative/previous speech. + base = len(self.chat.history) + from ..tools.approval import approval_turn + approval_token = approval_turn.set(turn_id) + except BaseException: + self._turn_lock.release() # A leaked lock would refuse every later turn until restart. + raise try: settings = self.config.raw.get("voice", {}) + # Validate voice settings before touching turn state: a bad value must fail this turn + # cleanly, not leave the previous turn's interjections attached to the new one. + interjections = Interjections(self.cancel, + float(settings.get("interruption_seconds", 1.5)), + float(settings.get("interjection_debounce_seconds", 1.0))) with self._voice_lock: self._cancel.clear() self._assertive_until = 0.0 @@ -426,11 +446,10 @@ def respond(self, text: str, user_name="User", *, record_user=True, speak=True, self._input_turn_id = turn_id self._input_continued = False self._spoken_offset = self._speech_cursor = 0 + self._speech_failed = False self._playing = None self._cutoff = self._history_start = self._history_end = None - self._interjections = Interjections(self.cancel, - float(settings.get("interruption_seconds", 1.5)), - float(settings.get("interjection_debounce_seconds", 1.0))) + self._interjections = interjections if record_user: event_bus.publish("chat.input", turn_id=turn_id, text=text, user_name=user_name, **origin) event_bus.publish("model.started", turn_id=turn_id, **origin) @@ -478,21 +497,17 @@ def on_delta(delta): event_bus.publish("chat.completed", turn_id=turn_id, text=self._generated, **origin) return response except TurnCancelled: - if self._closed: raise - with self._voice_lock: - # Replace, rather than append: ChatService may have committed just - # before cancellation arrived. Never duplicate the original turn. - prior_user = self.chat.history[base] if record_user and len(self.chat.history) > base and self.chat.history[base].role == 'user' else ChatMessage("user", f"{user_name}: {text}") - self.chat.history[base:] = [*([prior_user] if record_user else []), - *self._response_history(self._generated)] - self._history_start = base + int(record_user) - self._history_end = len(self.chat.history) - self._rewrite_user_input() - self.chat._save_history() - event_bus.publish("chat.cancelled", turn_id=turn_id, **origin) + # Shutdown cancels too: still keep what the user said and what they already saw or heard. + self._preserve_turn(base, turn_id, text, user_name, record_user, origin) + if not self._closed: event_bus.publish("chat.cancelled", turn_id=turn_id, **origin) raise except Exception as exc: - self.cancel() + # A failed reply is not a user interruption: stop output without cutting history, + # then keep the user's message (and any partial reply) so the model sees what was asked. + # Neither step may hide the original error from the caller or the model.error event. + try: self._stop(interrupt=False) + except Exception: logger.exception('Could not stop output after a failed reply') + self._preserve_turn(base, turn_id, text, user_name, record_user, origin) if not self._closed: event_bus.publish("model.error", turn_id=turn_id, error=str(exc), **origin) raise finally: @@ -507,19 +522,51 @@ def on_delta(delta): self._turn_lock.release() approval_turn.reset(approval_token) + def _preserve_turn(self, base, turn_id, text, user_name, record_user, origin): + """Keep an unfinished turn in history. Never raises: callers re-raise the real reason.""" + try: + with self._voice_lock: + # Replace, rather than append: ChatService may have committed just before the + # turn was cancelled or failed. Never duplicate the original turn. + history = self.chat.history + prior_user = history[base] if record_user and len(history) > base and history[base].role == 'user' else ChatMessage( + "user", f"{user_name}: {text}", source=origin.get('source'), conversation_id=origin.get('conversation_id')) + reply = self._response_history(self._generated) if self._active_turn == turn_id else [] + history[base:] = [*([prior_user] if record_user else []), *reply] + self._history_start = base + int(record_user) + self._history_end = len(history) + self._rewrite_user_input() + self.chat._save_history() + except Exception: logger.exception('Could not keep the unfinished turn in chat history') + + def _turn_in_flight(self): + return bool(self._active_turn and not self._interrupt_notified + and (self._generation_active or self._playing or self._speech_pending)) + + def _speech_heard(self): + """False when speech is muted or TTS failed this turn: the user read the text instead.""" + return bool(self._turn_speak and getattr(self.state, 'audio_enabled', True) and not self._speech_failed) + def cancel(self): + self._stop(interrupt=True) + + def _stop(self, *, interrupt): worker = getattr(self.chat, 'emotion_worker', None) if worker: worker.invalidate() with self._voice_lock: self._cancel.set() self._assertive_until = 0.0 - if self._active_turn and not self._interrupt_notified: - self._cutoff = self._audible_offset() if self._turn_speak else len(self._generated) + # Only a reply still generating or playing is interrupted. A finished turn stays + # intact when Stop, Sleep or shutdown arrive afterwards. + if interrupt and self._turn_in_flight(): + heard = self._speech_heard() + self._cutoff = self._audible_offset() if heard else len(self._generated) self._interrupt_notified = True - self._rewrite_history() + try: self._rewrite_history() + except Exception: logger.exception('Could not save chat history after an interruption') event_bus.publish("chat.interrupted", turn_id=self._active_turn, text=self._generated, offset=self._cutoff, - alignment="estimated_words" if self._turn_speak else "generated_text") + alignment="estimated_words" if heard else "generated_text") self._playing = None self._speech_pending = 0 if not self._generation_active: self.wake.response_finished() @@ -537,10 +584,11 @@ def cancel(self): def close(self): if self._closed: return self._closed = True - self.cancel() - self.state.set_speech('', seconds=0) + # Unsubscribe first, so a stalled cancel() can never block bus publishers on this session. self._unsubscribe_speech() self._unsubscribe_wake_feedback() + self.cancel() + self.state.set_speech('', seconds=0) for resource in (self.animation, self.initiative, self.voice, self.speech, self.wake): if resource: close_bounded(resource) self.voice = None diff --git a/Code/process/app_core/tools/approval.py b/Code/process/app_core/tools/approval.py index 93ccaef..dadcd3f 100644 --- a/Code/process/app_core/tools/approval.py +++ b/Code/process/app_core/tools/approval.py @@ -2,12 +2,16 @@ from copy import deepcopy from contextvars import ContextVar import json +import logging +import os import threading import time import uuid from ..events.bus import event_bus +from ..persistence.preserve import preserve_unreadable approval_turn = ContextVar('approval_turn', default=None) +logger = logging.getLogger(__name__) class ToolApprovals: @@ -17,26 +21,43 @@ def __init__(self, path, default=False): self.condition = threading.Condition(self.lock) self.pending, self.policy = {}, {} self.closed = False + self.error = '' try: value = json.loads(path.read_text(encoding='utf-8')) + if not isinstance(value, dict): raise ValueError('Approval policy is not a JSON object') self.policy = {k: v for k, v in value.items() if isinstance(k, str) and type(v) is bool} - except (OSError, ValueError, AttributeError): pass + except FileNotFoundError: pass + except (OSError, ValueError) as exc: + # Fail closed: an unreadable policy must not silently switch every approval off. + self.error = f'{path.name} could not be read ({exc}); every tool needs approval until the policy is saved again' + logger.error('Tool approval policy unreadable: %s', self.error) def snapshot(self): with self.lock: - return {'default_required': self.default, 'policy': dict(self.policy), + # Report the enforced default: while the policy is unreadable every tool needs approval. + return {'default_required': self.default or bool(self.error), 'policy': dict(self.policy), 'error': self.error, 'pending': [deepcopy(v['request']) for v in self.pending.values() if v['decision'] is None]} def configure(self, policy, names): if not isinstance(policy, dict) or any(k not in names or type(v) is not bool for k, v in policy.items()): raise ValueError('Approval policy must map registered tool names to booleans') with self.lock: - updated = {**self.policy, **policy} + if self.error: + # Keep the unreadable original (raises OSError if impossible, so it is never + # overwritten), and persist "require approval" for every tool the user did not + # just change, rather than dropping back to the permissive default. + preserve_unreadable(self.path) + base = {name: True for name in names} + else: base = self.policy + updated = {**base, **policy} self.path.parent.mkdir(parents=True, exist_ok=True) temporary = self.path.with_suffix('.tmp') - temporary.write_text(json.dumps(updated, indent=2), encoding='utf-8') + with temporary.open('w', encoding='utf-8') as stream: + stream.write(json.dumps(updated, indent=2)) + stream.flush() + os.fsync(stream.fileno()) temporary.replace(self.path) - self.policy = updated + self.policy, self.error = updated, '' event_bus.publish('tool.approval_policy') return self.snapshot() @@ -53,7 +74,7 @@ def resolve(self, request_id, approved): def authorize(self, name, arguments, call_id, cancelled=lambda: False, timeout=120): with self.lock: if self.closed or cancelled(): return False - if not self.policy.get(name, self.default): return True + if not self.error and not self.policy.get(name, self.default): return True request_id = str(uuid.uuid4()) request = {'id': request_id, 'name': name, 'arguments': deepcopy(arguments), 'call_id': call_id, 'expires_at': time.time() + timeout} diff --git a/Code/process/app_core/tools/builtin/todo_list.py b/Code/process/app_core/tools/builtin/todo_list.py index abec072..312c9a1 100644 --- a/Code/process/app_core/tools/builtin/todo_list.py +++ b/Code/process/app_core/tools/builtin/todo_list.py @@ -21,7 +21,7 @@ class Tool(BaseTool): complete - Mark a task as done (toggle). clear - Delete all tasks. - Task IDs are shown in the list output (e.g., "[1] Buy milk"). + Task IDs are shown in the list output (e.g., "1. Buy milk") and never change, even after removals. Tasks are stored in './persistent_memories/mcp_modules/todo_list/tasks.json'. Examples: @@ -41,19 +41,46 @@ def _ensure_storage(self) -> None: self.DATA_DIR.mkdir(parents=True, exist_ok=True) def _load_tasks(self) -> List[Dict[str, Any]]: - """Load tasks from the JSON file, or return an empty list.""" + """Load tasks. An unreadable list is kept aside and reported, never silently replaced.""" self._ensure_storage() + self._next_id = 1 if not self.DATA_FILE.exists(): return [] try: with open(self.DATA_FILE, "r", encoding="utf-8") as f: - tasks = json.load(f) - if isinstance(tasks, list): - return tasks - else: - return [] - except (json.JSONDecodeError, IOError): - return [] + data = json.load(f) + if isinstance(data, list): data = {"tasks": data} # Saved before the ID counter existed. + tasks = data.get("tasks") if isinstance(data, dict) else None + if not isinstance(tasks, list) or not all(isinstance(t, dict) and isinstance(t.get("text"), str) for t in tasks): + raise ValueError("not a task list") + except OSError as exc: + raise RuntimeError(f"The to-do list could not be read ({exc}); nothing was changed.") from exc + except ValueError as exc: + backup = self.DATA_FILE.with_name(f"tasks.json.unreadable-{datetime.now():%Y%m%d-%H%M%S-%f}") + self.DATA_FILE.replace(backup) + raise RuntimeError(f"The to-do list file was unreadable and was kept as {backup.name}; " + "a new list starts with the next change.") from exc + # Stable IDs that are never reused: the counter survives removals and clear. Lists saved + # before IDs existed are numbered in their current order; duplicate IDs get fresh ones. + next_id = max((t["id"] for t in tasks if type(t.get("id")) is int), default=0) + 1 + if type(data.get("next_id")) is int: next_id = max(next_id, data["next_id"]) + seen = set() + for t in tasks: + if type(t.get("id")) is not int or t["id"] in seen: + t["id"], next_id = next_id, next_id + 1 + seen.add(t["id"]) + self._next_id = next_id + return tasks + + @staticmethod + def _find(tasks: List[Dict[str, Any]], task_id: Optional[str]) -> Dict[str, Any]: + if task_id is None or not str(task_id).strip(): + raise ValueError("Please provide a task ID from the list (e.g., task_id=\"2\").") + try: wanted = int(str(task_id).strip()) + except ValueError: raise ValueError(f"Invalid task ID: '{task_id}'. Please use the number shown in the list.") from None + for t in tasks: + if t["id"] == wanted: return t + raise ValueError(f"Task ID {task_id} not found.") def _save_tasks(self, tasks: List[Dict[str, Any]]) -> None: """Save tasks to the JSON file atomically (via temp file).""" @@ -67,7 +94,7 @@ def _save_tasks(self, tasks: List[Dict[str, Any]]) -> None: delete=False, encoding="utf-8" ) as tmp: - json.dump(tasks, tmp, indent=2) + json.dump({"next_id": self._next_id, "tasks": tasks}, tmp, indent=2) tmp.flush() os.fsync(tmp.fileno()) temp_path = pathlib.Path(tmp.name) @@ -88,48 +115,35 @@ def _call(self, action: str, task: Optional[str] = None, task_id: Optional[str] if not tasks: return "📭 Your to‑do list is empty." lines = ["📋 **Your To‑Do List:**"] - for idx, t in enumerate(tasks, start=1): + for t in tasks: status = "✅" if t.get("done", False) else "⬜" - lines.append(f"{idx}. {status} {t['text']}") + lines.append(f"{t['id']}. {status} {t['text']}") return "\n".join(lines) elif action == "add": if not task or not task.strip(): - return "❌ Please provide a task description (e.g., `add Buy milk`)." + raise ValueError("Please provide a task description (e.g., task=\"Buy milk\").") + new_id, self._next_id = self._next_id, self._next_id + 1 tasks.append({ + "id": new_id, "text": task.strip(), "done": False, "created_at": datetime.now().isoformat(timespec="minutes") }) self._save_tasks(tasks) - return f"✅ Added task: '{task.strip()}' (ID: {len(tasks)})" + return f"✅ Added task: '{task.strip()}' (ID: {new_id})" elif action == "complete": - if task_id is None or not task_id.strip(): - return "❌ Please provide a task ID (e.g., `complete 2`)." - try: - idx = int(task_id.strip()) - 1 - if idx < 0 or idx >= len(tasks): - return f"❌ Task ID {task_id} not found." - tasks[idx]["done"] = not tasks[idx].get("done", False) - status = "done" if tasks[idx]["done"] else "undone" - self._save_tasks(tasks) - return f"✅ Task {task_id} marked as {status}." - except ValueError: - return f"❌ Invalid task ID: '{task_id}'. Please use a number." + found = self._find(tasks, task_id) + found["done"] = not found.get("done", False) + self._save_tasks(tasks) + return f"✅ Task {found['id']} marked as {'done' if found['done'] else 'undone'}." elif action == "remove": - if task_id is None or not task_id.strip(): - return "❌ Please provide a task ID (e.g., `remove 2`)." - try: - idx = int(task_id.strip()) - 1 - if idx < 0 or idx >= len(tasks): - return f"❌ Task ID {task_id} not found." - removed = tasks.pop(idx) - self._save_tasks(tasks) - return f"✅ Removed task: '{removed['text']}'" - except ValueError: - return f"❌ Invalid task ID: '{task_id}'. Please use a number." + found = self._find(tasks, task_id) + tasks.remove(found) + self._save_tasks(tasks) + return f"✅ Removed task {found['id']}: '{found['text']}'" elif action == "clear": if not tasks: @@ -138,7 +152,7 @@ def _call(self, action: str, task: Optional[str] = None, task_id: Optional[str] return "🗑️ All tasks have been cleared." else: - return f"❌ Unknown action: '{action}'. Available: list, add, complete, remove, clear." + raise ValueError(f"Unknown action: '{action}'. Available: list, add, complete, remove, clear.") # ------------------------------------------------------------------ # (Optional) Initialisation: ensure directory exists diff --git a/electron/src/tool_approvals.jsx b/electron/src/tool_approvals.jsx index 2a80ec1..40c5b84 100644 --- a/electron/src/tool_approvals.jsx +++ b/electron/src/tool_approvals.jsx @@ -15,5 +15,5 @@ export function ApprovalBubble({overlay=false}){ export function ToolApprovalSettings(){ const [data,setData,error,setError]=useApprovals(),[busy,setBusy]=useState(false); async function change(name,value){setBusy(true);try{const updated=await request('/api/tools/approvals',{method:'PUT',body:{policy:{[name]:value}}});setData(old=>({...old,...updated}));setError('');}catch(e){setError(e.message);}finally{setBusy(false);}} - return

Tool permissions

Require approval before each call. Applies immediately and persists across restarts. New tools follow the global approval default.

{error&&

{error}

}
{data?.tools.map(tool=>)}
; + return

Tool permissions

Require approval before each call. Applies immediately and persists across restarts. New tools follow the global approval default.

{data?.error&&

{data.error}

}{error&&

{error}

}
{data?.tools.map(tool=>)}
; } diff --git a/tests/test_data_integrity.py b/tests/test_data_integrity.py new file mode 100644 index 0000000..6068203 --- /dev/null +++ b/tests/test_data_integrity.py @@ -0,0 +1,350 @@ +"""User data is never silently erased or overwritten: finished replies survive Stop/Sleep/shutdown, +failed or interrupted turns keep the user's message, and unreadable stores fail closed.""" +import asyncio +import json +import shutil +from pathlib import Path +from types import SimpleNamespace + +import pytest +import torch + +from process.app_core.conversation.chat import ChatService +from process.app_core.conversation.messages import ChatMessage, ModelResponse +from process.app_core.desktop.state import DesktopState +from process.app_core.emotion.julia import JuliaEmotionEngine +from process.app_core.emotion.probe import EmotionProbe, ProbeConfig, build_network +from process.app_core.events.bus import EventBus, RuntimeEvent, event_bus +from process.app_core.runtime.cancellation import TurnCancelled +from process.app_core.runtime.session import SessionManager +from process.app_core.tools.approval import ToolApprovals + + +class FakeSpeech: + """Accepts nothing, like muted audio or an offline GPT-SoVITS.""" + def __init__(self, *args): pass + def submit(self, *args): return False + def submit_clip(self, *args, **kwargs): return False + def cancel(self): pass + def close(self): pass + + +@pytest.fixture +def session_parts(monkeypatch): + monkeypatch.setattr('process.app_core.runtime.session.SpeechQueue', FakeSpeech) + chat = SimpleNamespace(history=[], _save_history=lambda: None, provider=SimpleNamespace(close=lambda: None)) + config = SimpleNamespace(raw={}, root=Path('.'), character_name='Riko', tools=SimpleNamespace(max_iterations=8)) + session = SessionManager(config, chat, DesktopState()) + events = [] + unsubscribe = event_bus.subscribe(lambda event: events.append(event.type)) + yield session, chat, events + unsubscribe() + session.close() + + +def answer(chat, text): + def respond(user_text, user_name, **kwargs): + kwargs['on_delta'](text) + chat.history.extend([ChatMessage('user', f'{user_name}: {user_text}'), *kwargs['response_history'](text)]) + return ModelResponse(ChatMessage('assistant', text)) + return respond + + +def contents(chat): return [(message.role, message.content) for message in chat.history] + + +def test_stop_sleep_and_shutdown_after_a_finished_reply_keep_it(session_parts): + session, chat, events = session_parts + chat.respond = answer(chat, 'A complete answer.') + session.respond('hello') + finished = contents(chat) + session.cancel() # Stop or Sleep after the reply ended + session.close() # backend shutdown + assert contents(chat) == finished == [('user', 'User: hello'), ('assistant', 'A complete answer.')] + assert 'chat.interrupted' not in events + + +def test_interrupting_a_reply_whose_speech_failed_keeps_the_text_the_user_read(session_parts): + session, chat, _ = session_parts + def respond(text, user_name, **kwargs): + kwargs['on_delta']('Visible but never spoken. ') + session._playback_event(RuntimeEvent('speech.error', {'error': 'TTS offline'}, turn_id=session._active_turn)) + session.cancel() + raise TurnCancelled() + chat.respond = respond + with pytest.raises(TurnCancelled): session.respond('hello') + assert contents(chat) == [('user', 'User: hello'), ('assistant', 'Visible but never spoken. ')] + + +def test_a_failed_reply_keeps_the_users_message_and_is_not_an_interruption(session_parts): + session, chat, events = session_parts + def respond(text, user_name, **kwargs): + kwargs['on_delta']('Partial ') + raise RuntimeError('Maximum tool-call iterations exceeded') + chat.respond = respond + with pytest.raises(RuntimeError): session.respond('hello') + assert contents(chat) == [('user', 'User: hello'), ('assistant', 'Partial ')] + assert 'model.error' in events and 'chat.interrupted' not in events + + +def test_shutdown_during_a_reply_keeps_the_users_message(session_parts): + session, chat, _ = session_parts + def respond(text, user_name, **kwargs): + kwargs['on_delta']('Half an ans') + session.close() # shutdown arrives mid-reply + raise TurnCancelled() + chat.respond = respond + with pytest.raises(TurnCancelled): session.respond('hello') + assert chat.history[0].content == 'User: hello' + + +def test_a_bad_voice_setting_fails_the_turn_with_the_real_error_and_keeps_the_message(session_parts): + session, chat, events = session_parts + session.config.raw['voice'] = {'interjection_debounce_seconds': 0} # hand-edited YAML + with pytest.raises(ValueError, match='debounce'): session.respond('hello') + assert contents(chat) == [('user', 'User: hello')] + assert 'model.error' in events + + +def test_only_real_speech_failures_count_as_not_heard(session_parts): + session, _, _ = session_parts + session._active_turn = 'turn' + session._playback_event(RuntimeEvent('speech.error', {'error': 'Speech queue full', 'queued': False}, turn_id='turn')) + assert session._speech_heard() # later sentences still play + session._playback_event(RuntimeEvent('speech.error', {'error': 'TTS offline'}, turn_id='turn')) + assert not session._speech_heard() + + +def test_muted_voice_continuation_drops_the_cut_reply_fragment(session_parts): + session, chat, _ = session_parts + session.state.audio_enabled = False + def respond(text, user_name, **kwargs): + anchor = session.voice_anchor() # the user is still talking; nothing is visible yet + kwargs['on_delta']('Sure, the capital of') + assert session.voice_transcript('of France', 1, 2, anchor) + raise TurnCancelled() + chat.respond = respond + with pytest.raises(TurnCancelled): session.respond('What is the capital') + assert contents(chat) == [('user', 'User: What is the capital\nof France')] + + +def test_failed_replies_are_archived_as_errors_not_interruptions(tmp_path): + from process.app_core.persistence.conversation_store import ConversationStore + store = ConversationStore(tmp_path / 'conversations.sqlite3') + try: + store.observe(RuntimeEvent('chat.delta', {'text': 'partial'}, turn_id='turn')) + store.observe(RuntimeEvent('model.error', {'error': 'provider failed'}, turn_id='turn')) + message = store.page()['messages'][-1] + assert message['status'] == 'error' and not message.get('interrupted') + finally: store.close() + + +def test_turn_lock_is_released_when_superseding_previous_speech_fails(session_parts): + session, chat, _ = session_parts + session._speech_pending = 1 # the previous reply is still queued for playback + def broken(): raise OSError('audio device busy') + session.speech.cancel = broken + with pytest.raises(OSError): session.respond('first') + session.speech.cancel = lambda: None + chat.respond = answer(chat, 'Second reply.') + session.respond('second') # must not fail with "already handling another turn" + assert chat.history[-1].content == 'Second reply.' + + +def provider(text='hi'): + return SimpleNamespace(generate=lambda *args, **kwargs: ModelResponse(ChatMessage('assistant', text))) + + +def test_unreadable_chat_history_is_kept_aside_before_the_next_save(tmp_path): + path = tmp_path / 'chat_history.json' + path.write_text('[{"role": "user", "content": "keep me"},]', encoding='utf-8') # hand edit left a trailing comma + original = path.read_bytes() + service = ChatService(provider(), system_prompt='test', history_file=path) + assert service.history == [] + service.respond('hello') + backups = list(tmp_path.glob('chat_history.json.unreadable-*')) + assert len(backups) == 1 and backups[0].read_bytes() == original + assert [record['content'] for record in json.loads(path.read_text(encoding='utf-8'))] == ['User: hello', 'hi'] + + +def test_chat_history_is_never_saved_over_a_file_that_could_not_be_kept(tmp_path, monkeypatch): + path = tmp_path / 'chat_history.json' + path.write_text('{"not": "a list"}', encoding='utf-8') + original = path.read_bytes() + def locked(path): raise PermissionError('locked by another program') + monkeypatch.setattr('process.app_core.conversation.chat.preserve_unreadable', locked) + service = ChatService(provider(), system_prompt='test', history_file=path) + service.respond('hello') + assert path.read_bytes() == original + + +def test_permission_denied_files_are_moved_aside_instead_of_blocking_forever(tmp_path): + history, approvals = tmp_path / 'chat_history.json', tmp_path / 'tool_approvals.json' + history.write_text('[]', encoding='utf-8') + approvals.write_text('{"calculator": true}', encoding='utf-8') + for path in (history, approvals): path.chmod(0) # e.g. left behind by a run under another account + try: + service = ChatService(provider(), system_prompt='test', history_file=history) + service.respond('hello') + gate = ToolApprovals(approvals) + try: + assert gate.snapshot()['default_required'] is True # the UI shows what is enforced + gate.configure({'calculator': False}, names=['calculator', 'task_create']) + finally: gate.close() + finally: + for kept in tmp_path.glob('*.unreadable-*'): kept.chmod(0o600) + assert [record['content'] for record in json.loads(history.read_text(encoding='utf-8'))] == ['User: hello', 'hi'] + assert json.loads(approvals.read_text(encoding='utf-8')) == {'calculator': False, 'task_create': True} + assert {kept.name.split('.unreadable-')[0] for kept in tmp_path.glob('*.unreadable-*')} == {history.name, approvals.name} + + +def test_unreadable_tool_approval_policy_requires_approval_for_every_tool(tmp_path): + path = tmp_path / 'tool_approvals.json' + path.write_text('{"task_create": true,', encoding='utf-8') + gate = ToolApprovals(path, default=False) + try: + assert gate.snapshot()['error'] + assert gate.authorize('calculator', {}, 'call-1', timeout=0.05) is False # not silently allowed + gate.configure({'calculator': False}, names=['calculator', 'task_create']) + assert json.loads(path.read_text(encoding='utf-8')) == {'calculator': False, 'task_create': True} + assert gate.snapshot()['error'] == '' + assert len(list(tmp_path.glob('tool_approvals.json.unreadable-*'))) == 1 + finally: gate.close() + + +@pytest.fixture +def todo(monkeypatch, tmp_path): + from process.app_core.tools.builtin import todo_list + monkeypatch.setattr(todo_list.Tool, 'DATA_DIR', tmp_path) + monkeypatch.setattr(todo_list.Tool, 'DATA_FILE', tmp_path / 'tasks.json') + return todo_list.Tool({}) + + +def saved_tasks(tmp_path): return json.loads((tmp_path / 'tasks.json').read_text(encoding='utf-8'))['tasks'] + + +def test_todo_ids_stay_stable_across_removals_in_one_reply(todo, tmp_path): + for text in ('one', 'two', 'three', 'four'): todo.execute(action='add', task=text) + todo.execute(action='remove', task_id='2') + todo.execute(action='remove', task_id='3') # the model's second call, using IDs from the same list + assert [task['text'] for task in saved_tasks(tmp_path)] == ['one', 'four'] + + +def test_todo_ids_are_never_reused_after_removal_or_clear(todo, tmp_path): + for text in ('one', 'two', 'three'): todo.execute(action='add', task=text) + todo.execute(action='remove', task_id='3') + assert '(ID: 4)' in todo.execute(action='add', task='four') + with pytest.raises(ValueError): todo.execute(action='complete', task_id='3') # stale ID: no silent match + todo.execute(action='clear') + assert '(ID: 5)' in todo.execute(action='add', task='five') + + +def test_todo_lists_saved_before_ids_keep_their_numbers(todo, tmp_path): + (tmp_path / 'tasks.json').write_text(json.dumps([{'text': 'a', 'done': False}, {'text': 'b', 'done': False}]), encoding='utf-8') + todo.execute(action='complete', task_id='2') + assert [(task['id'], task['done']) for task in saved_tasks(tmp_path)] == [(1, False), (2, True)] + + +def test_unreadable_todo_list_is_kept_and_reported_not_replaced(todo, tmp_path): + (tmp_path / 'tasks.json').write_text('[{"text": "buy milk"', encoding='utf-8') + original = (tmp_path / 'tasks.json').read_bytes() + with pytest.raises(RuntimeError, match='kept as'): todo.execute(action='add', task='new') + backups = list(tmp_path.glob('tasks.json.unreadable-*')) + assert len(backups) == 1 and backups[0].read_bytes() == original + with pytest.raises(ValueError): todo.execute(action='remove', task_id='9') # bad input is a tool error + + +def teacher(): + engine = JuliaEmotionEngine(None) + engine._load_attempted = True + engine._model = SimpleNamespace(predict=lambda **kw: {'answers': { + 'emotion': {'choice': 'love', 'max_probability': .9}, 'intensity': {'score': 2}, 'valence': {'score': 3}}}) + return engine + + +def probe_config(): + return ProbeConfig.from_raw({'hidden_units': [32, 16], 'rank': 8, 'min_samples': 32, 'retrain_every': 32, 'epochs': 2}) + + +def trained_probe(directory, training): + probe = EmotionProbe(directory, {'model': 'one'}, teacher(), probe_config(), idle=lambda: False, training_directory=training) + probe.samples.append((torch.ones(256), [0, .5, 0., .5], 'message-one')) + probe.network = build_network(probe.config).eval().requires_grad_(False) + probe.validation = dict(agreement=.9, macro_f1=.8, rmse=.1, classes=3, validation_samples=32) # passes the defaults + probe.close() # writes training.pt, examples.json and probe.pt (weights) + return probe + + +def reopen(directory, training, **config): + config = ProbeConfig.from_raw({'hidden_units': [32, 16], 'rank': 8, 'min_samples': 32, 'retrain_every': 32, 'epochs': 2, **config}) + return EmotionProbe(directory, {'model': 'one'}, teacher(), config, idle=lambda: False, training_directory=training) + + +def test_trained_weights_survive_a_restart_with_stricter_thresholds(tmp_path): + directory, training = tmp_path / 'probe', tmp_path / 'training' + trained_probe(directory, training) + shutil.rmtree(training) # the weights in probe.pt are now the only copy + strict = reopen(directory, training, min_macro_f1=0.95) + strict.validation = {} # something saved later (e.g. an unqualified training run) + strict.close() + assert not strict.ready + restored = reopen(directory, training) # thresholds relaxed again + try: assert restored.ready + finally: restored.close() + + +def test_unreadable_legacy_probe_file_is_ignored_not_moved_and_saving_still_works(tmp_path): + legacy = tmp_path / 'legacy' + probe = EmotionProbe(tmp_path / 'probe', {'model': 'one'}, teacher(), probe_config(), idle=lambda: False, + training_directory=tmp_path / 'training', legacy_directory=legacy) + probe.close() + probe.legacy_path.parent.mkdir(parents=True, exist_ok=True) + probe.legacy_path.write_bytes(b'written by an old build') + probe.path.unlink(); probe.data_path.unlink() # only the unreadable legacy file is left + restored = EmotionProbe(tmp_path / 'probe', {'model': 'one'}, teacher(), probe_config(), idle=lambda: False, + training_directory=tmp_path / 'training', legacy_directory=legacy) + restored.close() + assert not restored.save_blocked and restored.path.exists() + assert probe.legacy_path.read_bytes() == b'written by an old build' + + +def test_unreadable_probe_training_data_is_kept_and_the_weights_still_restore(tmp_path): + directory, training = tmp_path / 'probe', tmp_path / 'training' + first = trained_probe(directory, training) + first.data_path.write_bytes(b'truncated by a disk error') + restored = reopen(directory, training) + try: + assert restored.ready and restored.error + finally: restored.close() + kept = list(first.data_path.parent.glob('training.pt.unreadable-*')) + assert len(kept) == 1 and kept[0].read_bytes() == b'truncated by a disk error' + + +def test_trained_probe_restores_after_its_training_data_was_deleted(tmp_path): + directory, training = tmp_path / 'probe', tmp_path / 'training' + trained_probe(directory, training) + shutil.rmtree(training) + restored = reopen(directory, training) + try: assert restored.ready and len(restored.samples) == 0 + finally: restored.close() + assert torch.load(restored.path, weights_only=True)['weights'] is not None # shutdown kept the weights + + +def test_event_socket_bootstrap_builds_its_snapshot_off_the_event_loop(): + from process.app_core.events.stream import stream_events + seen = {} + def snapshot(): + try: asyncio.get_running_loop(); seen['on_loop'] = True + except RuntimeError: seen['on_loop'] = False + return {} + class Socket: + def __init__(self): self.sent = [] + async def accept(self): pass + async def send_json(self, data): self.sent.append(data) + async def receive(self): + await asyncio.sleep(0.2) + return {'type': 'websocket.disconnect'} + async def close(self, code=1000): pass + socket = Socket() + asyncio.run(stream_events(socket, EventBus(), snapshot)) + assert seen == {'on_loop': False} and socket.sent[0]['type'] == 'state.snapshot' From 956abdd221a4442b6e02ef047f43584dc351ac34 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 11:05:14 +0530 Subject: [PATCH 05/37] Replace the calculator's eval() with a restricted AST evaluator The scientific_calculator tool passed model-controlled text to eval(); attribute walking (().__class__.__base__.__subclasses__()) reached os.system, so prompt injection could run shell commands. It now evaluates only numbers, arithmetic operators, the listed constants and direct calls to the listed math functions, with bounds on input length, nesting, integer size (checked before powers) and complex results. Errors raise, so the registry reports them as tool errors. Fixes audit finding V003, plus issues found by an adversarial attack review (flat sums rejected as nesting, over-cautious power check, parser MemoryError leaking an empty message, unclear errors). Co-Authored-By: Claude Opus 5.5 --- .../tools/builtin/scientific_calculator.py | 146 ++++++++++++------ tests/test_scientific_calculator.py | 59 +++++++ 2 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 tests/test_scientific_calculator.py diff --git a/Code/process/app_core/tools/builtin/scientific_calculator.py b/Code/process/app_core/tools/builtin/scientific_calculator.py index f9b9b2e..2d83d30 100644 --- a/Code/process/app_core/tools/builtin/scientific_calculator.py +++ b/Code/process/app_core/tools/builtin/scientific_calculator.py @@ -1,6 +1,87 @@ +import ast import math +import operator from .base import BaseTool, ToolType +# Expressions come from model output, which prompt injection can control, so they are never +# passed to eval(). Only the syntax below is evaluated; everything else is rejected. +FUNCTIONS = {name: getattr(math, name) for name in ( + 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'log', 'log10', 'log2', 'exp', 'sqrt', 'pow', 'hypot')} +CONSTANTS = {'pi': math.pi, 'e': math.e, 'tau': math.tau, 'inf': math.inf, 'nan': math.nan} +BINARY = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, + ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow} +UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg} +MAX_LENGTH = 1000 # characters +MAX_DEPTH = 100 # nested operations +MAX_INT_BITS = 10_000 # ~3,000 digits; checked before exponentiation, so 9**9**9 cannot hang + + +def _evaluate(node, depth=0): + if depth > MAX_DEPTH: raise ValueError('Expression is nested too deeply') + if isinstance(node, ast.Constant) and type(node.value) in (int, float): + return node.value + if isinstance(node, ast.Name) and node.id in CONSTANTS: + return CONSTANTS[node.id] + if isinstance(node, ast.UnaryOp) and type(node.op) in UNARY: + return _checked(UNARY[type(node.op)](_evaluate(node.operand, depth + 1))) + if isinstance(node, ast.BinOp) and type(node.op) in BINARY: + if isinstance(node.op, ast.Pow): # Right-associative: recurse. + return _binary(node.op, _evaluate(node.left, depth + 1), _evaluate(node.right, depth + 1)) + # a + b + c ... nests leftwards; walk such chains iteratively so a long flat sum or + # product is not mistaken for deep nesting. + chain = [] + while isinstance(node, ast.BinOp) and type(node.op) in BINARY and not isinstance(node.op, ast.Pow): + chain.append(node) + node = node.left + value = _evaluate(node, depth + 1) + for link in reversed(chain): value = _binary(link.op, value, _evaluate(link.right, depth + 1)) + return value + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in FUNCTIONS and not node.keywords: + arguments = [_evaluate(argument, depth + 1) for argument in node.args] + try: value = FUNCTIONS[node.func.id](*arguments) + except ValueError as exc: raise ValueError(f'Math domain error in {node.func.id}(): {exc}') from None + return _checked(value) + raise ValueError(_describe(node)) + + +def _binary(op, left, right): + # Refuse only powers that are certainly too large (|left| >= 2**(bits-1)); anything that + # passes has under 2 * MAX_INT_BITS bits, computes in microseconds, and _checked enforces + # the exact limit, so 2**9998 and 2**4999 * 2**4999 agree. + if isinstance(op, ast.Pow) and isinstance(left, int) and isinstance(right, int) \ + and abs(left) > 1 and right * (left.bit_length() - 1) >= MAX_INT_BITS: + raise ValueError('Result is too large') + return _checked(BINARY[type(op)](left, right)) + + +def _describe(node): + if isinstance(node, ast.Name): + return f"Unknown name '{node.id}'; available constants: {', '.join(CONSTANTS)}" + if isinstance(node, ast.Call): + name = node.func.id if isinstance(node.func, ast.Name) else None + if name in FUNCTIONS: return f'{name}() takes positional arguments only' + return f"Unknown function{f' {name!r}' if name else ''}; available functions: {', '.join(FUNCTIONS)}" + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitXor): return "'^' is not supported; use ** for powers" + if isinstance(node, ast.Constant): return f'Only numbers are supported, not {type(node.value).__name__}' + return f'Unsupported syntax ({type(node).__name__}); use numbers, + - * / // % **, parentheses, constants and math functions' + + +def _checked(value): + if isinstance(value, complex): raise ValueError('Result is a complex number') + if isinstance(value, int) and value.bit_length() > MAX_INT_BITS: raise ValueError('Result is too large') + return value + + +def evaluate(expression): + if not isinstance(expression, str) or not expression.strip(): raise ValueError('Provide an expression') + if len(expression) > MAX_LENGTH: raise ValueError(f'Expression is longer than {MAX_LENGTH} characters') + # The parser reports overly complex input as MemoryError or RecursionError, sometimes with no message. + try: tree = ast.parse(expression.strip(), mode='eval') + except (SyntaxError, ValueError, MemoryError, RecursionError) as exc: + raise ValueError(f'Invalid expression: {str(exc) or "too complex to parse"}') from None + try: return _evaluate(tree.body) + except (ArithmeticError, TypeError, RecursionError) as exc: raise ValueError(f'Error evaluating expression: {exc}') from None + class Tool(BaseTool): TOOL_NAME = "scientific_calculator" @@ -9,71 +90,34 @@ class Tool(BaseTool): MCP_PROMPT = """scientific_calculator: Evaluates a mathematical expression using Python's math functions. - + Parameters: - expression (str) - The mathematical expression to evaluate. + expression (str) - The mathematical expression to evaluate. Use standard arithmetic operators and math functions. - + Supported functions and constants: - sin, cos, tan, asin, acos, atan, atan2, + sin, cos, tan, asin, acos, atan, atan2, log (natural), log10, log2, - exp, sqrt, pow, + exp, sqrt, pow, hypot, pi, e, tau, inf, nan - + Basic arithmetic: +, -, *, /, //, ** (exponentiation), % (modulo) Parentheses for grouping: ( ... ) Use ** for exponentiation (e.g., 2**10 = 1024) - + Examples: - scientific_calculator(expression="sin(pi/4) + log(100)") -> 1.7071067811865475 + scientific_calculator(expression="sin(pi/4) + log(100)") -> 5.31227696717464 scientific_calculator(expression="sqrt(2**10)") -> 32.0 - scientific_calculator(expression="5 * (3 + 2) - 4") -> 21.0 + scientific_calculator(expression="5 * (3 + 2) - 4") -> 21 scientific_calculator(expression="atan2(3, 4) * 180 / pi") -> 36.86989764584402 scientific_calculator(expression="pow(2, 8)") -> 256.0 scientific_calculator(expression="log10(1000)") -> 3.0 scientific_calculator(expression="exp(1)") -> 2.718281828459045 - - If the expression is invalid (syntax error, undefined function, etc.), - the tool returns an error message describing the problem. + + Anything else (names, attributes, strings, very large results) is rejected + with an error describing the problem. """ def _call(self, expression: str) -> str: #type: ignore - """ - Evaluate the mathematical expression safely using the math module. - Returns the result as a string for easy display. - """ - # Build a safe evaluation environment: only math functions and constants - safe_dict = { - # Mathematical functions - 'sin': math.sin, - 'cos': math.cos, - 'tan': math.tan, - 'asin': math.asin, - 'acos': math.acos, - 'atan': math.atan, - 'atan2': math.atan2, - 'log': math.log, - 'log10': math.log10, - 'log2': math.log2, - 'exp': math.exp, - 'sqrt': math.sqrt, - 'pow': math.pow, - 'hypot': math.hypot, - # Constants - 'pi': math.pi, - 'e': math.e, - 'tau': math.tau, - 'inf': math.inf, - 'nan': math.nan, - } - # Disable builtins for safety (prevent code injection) - safe_dict['__builtins__'] = None - - try: - # Evaluate the expression with the restricted namespace - result = eval(expression, safe_dict, {}) - # Format the result: convert to string with repr for precision - return repr(result) - except Exception as e: - # Return a user-friendly error message - return f"Error evaluating expression: {e}" + """Evaluate the expression with the restricted evaluator; errors raise ValueError.""" + return repr(evaluate(expression)) diff --git a/tests/test_scientific_calculator.py b/tests/test_scientific_calculator.py new file mode 100644 index 0000000..1ca6125 --- /dev/null +++ b/tests/test_scientific_calculator.py @@ -0,0 +1,59 @@ +"""The calculator evaluates model-controlled text, so it must never execute anything but arithmetic.""" +import math +import time + +import pytest + +from process.app_core.tools.builtin.scientific_calculator import Tool, evaluate + + +@pytest.mark.parametrize('expression, expected', [ + ('sin(pi/4) + log(100)', repr(math.sin(math.pi / 4) + math.log(100))), + ('sqrt(2**10)', '32.0'), ('5 * (3 + 2) - 4', '21'), ('atan2(3, 4) * 180 / pi', repr(math.atan2(3, 4) * 180 / math.pi)), + ('pow(2, 8)', '256.0'), ('log10(1000)', '3.0'), ('exp(1)', repr(math.e)), ('-3 % 5', '2'), ('7 // 2', '3'), + ('2 ** -2', '0.25'), ('hypot(3, 4)', '5.0'), ('+tau - 2*pi', '0.0'), +]) +def test_ordinary_arithmetic_still_works(expression, expected): + assert Tool({}).execute(expression=expression) == expected + + +@pytest.mark.parametrize('expression', [ + "().__class__.__base__.__subclasses__()", + "[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module" + ".__builtins__['__import__']('os').system('touch {marker}')", + "__import__('os').system('touch {marker}')", "(lambda: 1)()", "open('{marker}', 'w')", "'a' * 10", + "sqrt.__self__", "pi.real", "x", "sin(x=1)", "[1, 2]", "{{1: 2}}", "True + 1", "1 if 1 else 2", "1 < 2", +]) +def test_code_execution_and_non_arithmetic_syntax_are_rejected(expression, tmp_path): + marker = tmp_path / 'pwned' + with pytest.raises(ValueError): evaluate(expression.format(marker=marker)) + assert not marker.exists() + + +@pytest.mark.parametrize('expression', ['9**9**9', '10**4000', '(10**3000) * (10**3000)', '2**10**4', '2**10000', '3**9999']) +def test_huge_results_are_refused_quickly(expression): + started = time.perf_counter() + with pytest.raises(ValueError, match='too large'): evaluate(expression) + assert time.perf_counter() - started < 1 + + +def test_results_up_to_the_limit_agree_however_they_are_written(): + assert evaluate('2**9998') == evaluate('2**4999 * 2**4999') + assert evaluate('10**3000') == evaluate('(10**1500) * (10**1500)') + + +def test_long_flat_sums_and_products_are_not_mistaken_for_nesting(): + assert evaluate('+'.join(str(n) for n in range(1, 201))) == sum(range(1, 201)) # 691 characters, 200 terms + assert evaluate('*'.join(str(n) for n in range(1, 103))) == math.factorial(102) + + +@pytest.mark.parametrize('expression, message', [ + ('-' * 500 + '1', 'nested too deeply'), ('1+' * 600 + '1', 'longer than'), ('1/0', 'division by zero'), + ('(-8) ** 0.5', 'complex'), ('sqrt(-1)', 'Math domain error in sqrt'), ('log(0)', 'Math domain error in log'), + ('exp(1000)', 'range'), ('sqrt(1, 2)', 'argument'), ('', 'Provide'), ('1 +', 'Invalid'), + ('[' * 199 + ']' * 199 + ' 1', 'Invalid expression: .+'), ('(-' * 199 + ')' * 199, 'Invalid expression: .+'), + ('2^10', 'use \\*\\*'), ('factorial(5)', "Unknown function 'factorial'"), ('x + 1', "Unknown name 'x'"), + ('sin(x=1)', 'positional arguments only'), ("'a'", 'Only numbers'), +]) +def test_bad_input_is_a_clear_error(expression, message): + with pytest.raises(ValueError, match=message): evaluate(expression) From 793d8902c39aa4ff3e6a43b2c656e70eb4995cc4 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 14:39:27 +0530 Subject: [PATCH 06/37] Lock down the local API: per-start token, Host/Origin checks, signed confirmations Any local web page or process could drive the backend on 127.0.0.1:8765. Now every HTTP request and WebSocket handshake needs this backend start's bearer token and a loopback Host, and sockets need an app Origin (LocalAPIGuard). - run_server.py binds 127.0.0.1:8765 before the model loads, then mints the token and a separate confirmation key. Dev writes both owner-only under persistent_memories; packaged Electron generates them, passes them to the backend it spawns, and sends the token only after the backend prints RIKO_BACKEND_LISTENING. Both are removed from the backend's environment. - Electron main injects the header for 127.0.0.1:8765 only, so renderer code never sees the token; main's own calls use backendFetch. - Security-sensitive settings (code, models, libraries, programs, file access, network endpoints, judged by key, by each leaf of a section saved at once, and by URL/absolute-path values), turning tool approval off, and Discord access changes return 428 with a single-use challenge. Electron main shows a native dialog (hidden characters escaped) and signs it with the confirmation key, which never goes over the wire. - The Discord worker gets the token from its launcher, exits with its backend, talks only to 127.0.0.1 (localhost URLs are rewritten), and its socket now receives only Discord's own turns. - API docs are served only with RIKO_DEV=1. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 + CLAUDE.md | 10 +- Code/desktop_server.py | 84 +++++++- Code/discord_bot.py | 18 +- Code/process/app_core/desktop/api_guard.py | 145 +++++++++++++ .../app_core/integrations/discord/backend.py | 16 +- .../app_core/integrations/discord/commands.py | 2 + .../app_core/integrations/discord/config.py | 6 +- .../app_core/integrations/discord/launcher.py | 11 +- Code/run_server.py | 20 +- electron/main.cjs | 69 +++++- electron/preload.cjs | 2 +- electron/release.cjs | 27 ++- electron/src/api.mjs | 18 +- electron/src/release.test.mjs | 3 +- tests/test_desktop_api.py | 198 ++++++++++++++++-- tests/test_discord.py | 6 +- tests/test_discord_launcher.py | 16 +- 18 files changed, 599 insertions(+), 57 deletions(-) create mode 100644 Code/process/app_core/desktop/api_guard.py diff --git a/.gitignore b/.gitignore index 9aa7298..9686e17 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,11 @@ output.wav /release-stage/ /release-build/ /persistent_memories/discord_access.json +# Per-start backend secrets, wherever the data root is (and their write temporaries). +**/persistent_memories/api_token +**/persistent_memories/confirm_key +**/persistent_memories/.api_token.*.tmp +**/persistent_memories/.confirm_key.*.tmp /.native/ /character_config.yaml.previous /.settings-save-* diff --git a/CLAUDE.md b/CLAUDE.md index 44a2f8b..1dc7128 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. ### Startup and service assembly - `run_server.main` chdirs to `$RIKO_DATA_DIR` (default: repo root), loads config, configures logging, then imports `desktop_server`. That import itself calls `load_config()` and builds `DiscordLauncher`, `GPUMonitor` and `WhiteboardImages`, so tests stub `load_config` before the first import (see `tests/test_desktop_api.py`). - The FastAPI lifespan calls `create_chat_service` (`app_core/factory.py`), which builds everything inside an `ExitStack` that rolls back on failure: `ActionController` → optional Julia emotion engine → `create_provider` (+ optional emotion probe) → `provider.warmup()` (loads the GGUF, no timeout) → `TaskStore` / `ToolRegistry` → `MemoryStore` → `ChatService`. Then come `SessionManager`, `warm_session` (ASR/VAD/wake/TTS, bounded by `runtime.startup_timeout_seconds`), `ConversationStore`, `Initiative` and the resource/task-file watchers, and finally `runtime.ready` is published. -- uvicorn binds :8765 only after lifespan startup, so the API refuses connections while the model loads. +- `run_server.py` binds 127.0.0.1:8765 before the model loads (retrying for 20 s while a previous backend exits) and mints the API secrets right after; connections queue until lifespan startup finishes, so requests hang rather than fail while the model loads. - With `desktop.setup_on_startup_error: true`, a failing `create_chat_service` leaves only the settings/status/resource/media routes up (the rest return 503) so Settings can repair the YAML. - Shutdown: stop Discord → `close_bounded(session, 10)` (`app_core/runtime/lifecycle.py`; a hung close is abandoned to a daemon thread) → unwind the ExitStack. @@ -127,7 +127,13 @@ The data root is the directory holding `character_config.yaml`. - The backend's PyYAML parses YAML 1.1; ruamel and Electron's `yaml` parse 1.2. They disagree on `yes`/`no`/`on`/`off`. ### Electron ↔ backend contract -- `http://127.0.0.1:8765` is hard-coded in `electron/src/api.mjs`, `electron/main.cjs` and several components. There is no authentication; CORS allows `null` (the packaged `file://` renderer) and the Vite origin. +- `http://127.0.0.1:8765` is hard-coded in `electron/src/api.mjs`, `electron/main.cjs` and several components. CORS allows `null` (the packaged `file://` renderer) and the Vite origin. +- Every HTTP and WebSocket request needs `Authorization: Bearer ` and a loopback Host; WebSockets also need an app Origin (`LocalAPIGuard` in `app_core/desktop/api_guard.py`). The token and a separate confirmation key are fresh on every backend start, minted only after `run_server.py` has bound 127.0.0.1:8765 (before the model loads), and removed from the backend's environment so children never inherit them: + - Packaged: Electron generates both and passes `RIKO_API_TOKEN`/`RIKO_CONFIRM_KEY` to the backend it spawns; nothing is written to disk. Main sends the token only after the backend prints `RIKO_BACKEND_LISTENING` on stdout (`watchListening` in `electron/release.cjs`), and stops when it exits. + - Dev: the backend writes `persistent_memories/api_token` and `confirm_key` (mode 0600) beside the config; Electron main re-reads them when they change. + - Electron main injects the header for every renderer request to `127.0.0.1:8765` (`injectApiToken` in `electron/main.cjs`), so renderer code never handles the token; main's own requests use `backendFetch`. The Discord worker gets `RIKO_API_TOKEN` from its launcher and exits when its stdin pipe closes, i.e. with that backend (`RIKO_EXIT_WITH_BACKEND`); started by hand, `client_token()` reads the file on every reconnect. Use `127.0.0.1`, not `localhost`, which may resolve to `::1`. + - Tests use `client_for(backend)` in `tests/test_desktop_api.py`. API docs are served only with `RIKO_DEV=1`. +- Security-sensitive changes (`security_sensitive()` in `Code/desktop_server.py`: anything that loads code, models or libraries, starts programs, widens file access or sends data elsewhere, judged by key name, by every leaf of a section saved at once, and by values that are URLs or absolute paths; turning tool approval off; Discord access) return 428 with a single-use `confirm` challenge. `request()` in `electron/src/api.mjs` asks Electron main (`confirm-security-change`), which shows a native dialog and signs the challenge with the confirmation key, which never goes over the wire. A new setting that can do any of those things must be covered by `security_sensitive()`. - `/ws/events` (`app_core/events/stream.py`) sends `state.snapshot`, then `resource.snapshot`, then every bus event. A client with 100 queued events is closed with code 1013. - All windows share one socket (`electron/src/event_connection.mjs`, 1 s reconnect). Subscribe via `connectEvents`, `useRuntime` or `useResource`, and never poll: `electron/src/subscription_guard.test.mjs` forbids `setInterval`. - Every `DesktopState` change republishes a full `state.snapshot`. Panels read `resource.` events from `ResourceEvents` (`app_core/events/resources.py`); a new topic needs an `observe()` mapping and a `resource_getters()` entry. diff --git a/Code/desktop_server.py b/Code/desktop_server.py index f79cd60..2a048c3 100644 --- a/Code/desktop_server.py +++ b/Code/desktop_server.py @@ -4,8 +4,10 @@ import asyncio import json import logging +import os +import re from contextlib import asynccontextmanager -from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, HTTPException +from fastapi import FastAPI, Header, Request, WebSocket, WebSocketDisconnect, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field, StrictInt from starlette.concurrency import run_in_threadpool @@ -121,7 +123,45 @@ async def lifespan(_app): if conversation_store: conversation_store.close() event_bus.publish("runtime.stopped") -app = FastAPI(title="Riko Local Desktop Runtime", lifespan=lifespan) +_dev = os.environ.get('RIKO_DEV') == '1' # The API map is published only while developing. +app = FastAPI(title="Riko Local Desktop Runtime", lifespan=lifespan, + docs_url='/docs' if _dev else None, redoc_url='/redoc' if _dev else None, openapi_url='/openapi.json' if _dev else None) + +_secrets = {} +def api_secrets(): + """(token, confirmation key) for this start, minted once (see app_core/desktop/api_guard.py). + run_server mints them right after binding the port; tests mint them on first use.""" + from process.app_core.desktop.api_guard import issue_secrets + if config.root not in _secrets: _secrets[config.root] = issue_secrets(config.root) + return _secrets[config.root] + +def api_token(): return api_secrets()[0] +discord_launcher.token = api_token # The Discord worker it starts receives this start's token. + +from process.app_core.desktop.api_guard import Confirmations +confirmations = Confirmations() + +def require_confirmation(action, changes, provided): + """Security-sensitive changes need a single-use signature that Electron main makes with the + confirmation key, which never leaves the machine's files, after the user approves a dialog.""" + if not changes or confirmations.consume(api_secrets()[1], action, changes, provided): return + raise HTTPException(428, {'detail': 'Confirm this change in the Riko window', 'confirm': confirmations.challenge(action, changes)}) + +# Settings that load code, start programs, choose models or network endpoints, move user data, widen +# file access, or decide what runs unattended. Classified by name, so settings added later with these +# names are covered; any other value that is a URL or an absolute path is treated the same way. +SENSITIVE_SETTING_NAMES = {'native_library', 'mcp_config', 'executable', 'arguments', 'auto_start', 'api_key', 'url', + 'provider', 'model_id', 'embedding_model', 'asr_model', 'require_approval', 'system1_enabled'} +LOCATION = re.compile(r'\s*([a-z][a-z0-9+.-]*://|[/\\~]|[a-z]:[/\\])', re.IGNORECASE) +def security_sensitive(key, value=None): + # An empty mapping in the YAML is one editable value, so a whole section can arrive at once: check each leaf. + if isinstance(value, dict): return security_sensitive(key) or any(security_sensitive(f'{key}.{name}', item) for name, item in value.items()) + if isinstance(value, (list, tuple)): return security_sensitive(key) or any(security_sensitive(key, item) for item in value) + section, _, name = key.rpartition('.') + if (name in SENSITIVE_SETTING_NAMES or name.startswith('hf_') or key == 'emotion.enabled' + or name.endswith(('_path', '_dir', '_directory', '_folder', '_root', '_file', '_url', '_model_id', '_library', '_config', '_executable'))): + return True + return isinstance(value, str) and key != 'avatar.model' and bool(LOCATION.match(value)) # the avatar endpoint serves only .vrm files from process.app_core.integrations.discord.api import create_router as discord_router app.include_router(discord_router(lambda: session, lambda: discord_launcher)) @@ -149,7 +189,11 @@ class DiscordSettingsRequest(BaseModel): revision: str @app.put('/api/discord/settings') -def save_discord_settings(request: DiscordSettingsRequest): +def save_discord_settings(request: DiscordSettingsRequest, x_riko_confirmation: str | None = Header(default=None)): + # Allow-lists decide who can drive the companion from Discord: changes need confirmation. + try: current = discord_launcher.access.read()['values'] + except ValueError: current = {} + require_confirmation('discord_access', {key: value for key, value in request.values.items() if current.get(key) != value}, x_riko_confirmation) try: return discord_launcher.configure(request.values, request.revision) except ValueError as exc: raise HTTPException(400, str(exc)) from exc except RuntimeError as exc: raise HTTPException(409, str(exc)) from exc @@ -252,9 +296,21 @@ async def discord_client(websocket: WebSocket, instance: str): except (ValueError, TypeError): await websocket.accept(); await websocket.close(code=1008, reason='A Discord client is already connected or the instance ID is invalid'); return async def report(value): await run_in_threadpool(discord_launcher.report, instance, value) + # The Discord process sees only what it acts on: its own turns, approvals, Discord and + # initiative messages and whiteboard updates. Local transcripts, chats and desktop state stay local. + discord_turns = set() + def relevant(event): + if event.payload.get('source') == 'discord' and event.turn_id: + discord_turns.add(event.turn_id) + if len(discord_turns) > 256: discord_turns.clear(); discord_turns.add(event.turn_id) + if event.type in {'resource.discord', 'resource.approvals', 'tool.approval_finished', 'tool.approval_resolved', + 'initiative.presented', 'whiteboard.changed', 'whiteboard.image'}: return True + return event.turn_id in discord_turns and event.type in {'chat.delta', 'model.reasoning', 'tool.approval_requested'} + def resources(): + values = resource_events.snapshot() if resource_events else {'discord': discord_launcher.status()} + return {key: values[key] for key in ('discord', 'approvals') if key in values} try: - await stream_events(websocket, event_bus, snapshot, - initial=lambda: resource_events.snapshot() if resource_events else {'discord': discord_launcher.status()}, on_message=report) + await stream_events(websocket, event_bus, lambda: {}, initial=resources, event_filter=relevant, on_message=report) finally: # Shielded: detach must still free the client slot if this handler is being cancelled. await asyncio.shield(run_in_threadpool(discord_launcher.detach, instance)) @@ -274,9 +330,14 @@ def tool_approvals(): return {**registry.approvals.snapshot(), 'tools': [{'name': t.name, 'description': t.description} for t in registry.tools.values()]} @app.put('/api/tools/approvals') -def approval_policy(request: dict): +def approval_policy(request: dict, x_riko_confirmation: str | None = Header(default=None)): registry = approval_registry() - try: return registry.approvals.configure(request.get('policy'), registry.tools) + # Switching approval off lets the model run that tool unattended: confirm it. + policy, current = request.get('policy'), registry.approvals.snapshot() + if isinstance(policy, dict): + require_confirmation('tool_approvals', {name: False for name, value in policy.items() + if value is False and current['policy'].get(name, current['default_required'])}, x_riko_confirmation) + try: return registry.approvals.configure(policy, registry.tools) except ValueError as exc: raise HTTPException(400, str(exc)) except OSError as exc: raise HTTPException(409, f'Could not save tool permissions: {exc}') @@ -364,8 +425,12 @@ def validate_settings(request: SettingsPatch): except (ValueError, TypeError) as exc: raise HTTPException(400, str(exc)) @app.put('/api/settings') -def save_settings(request: SettingsPatch): +def save_settings(request: SettingsPatch, x_riko_confirmation: str | None = Header(default=None)): from process.app_core.configuration.settings_store import SettingsConflict + try: current = settings_store().snapshot()['values'] + except (OSError, ValueError, TypeError): current = {} + require_confirmation('settings', {key: value for key, value in request.changes.items() + if security_sensitive(key, value) and current.get(key) != value}, x_riko_confirmation) try: result = settings_store().save(request.changes, request.revision) if result.get('saved') and 'emotion.probe.interval_tokens' in request.changes: @@ -874,5 +939,8 @@ async def chat_stream(websocket: WebSocket): except (WebSocketDisconnect, RuntimeError): return +# Every route, HTTP and WebSocket, needs the install's API token and a loopback Host. +from process.app_core.desktop.api_guard import LocalAPIGuard +app.add_middleware(LocalAPIGuard, token=api_token) # Outermost: even setup-mode 503 responses need readable CORS headers. app.add_middleware(CORSMiddleware, allow_origins=["null", "http://localhost:5173", "http://127.0.0.1:5173"], allow_methods=["*"], allow_headers=["*"]) diff --git a/Code/discord_bot.py b/Code/discord_bot.py index f5ad857..37989fc 100644 --- a/Code/discord_bot.py +++ b/Code/discord_bot.py @@ -1,7 +1,19 @@ """Start the optional Discord client of the already-running Python backend.""" +import asyncio import logging import os from pathlib import Path +import sys +import threading + + +def exit_with_backend(bot): + """Started by the desktop backend: stop when its pipe closes, which happens however the backend exits.""" + try: sys.stdin.read() + except (OSError, ValueError): pass + try: asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(10) + except Exception: pass # not logged in yet, or already closing + os._exit(0) def main(): @@ -9,12 +21,16 @@ def main(): from process.app_core.integrations.discord.access import DiscordAccess from process.app_core.integrations.discord.bot import CompanionBot root = Path(os.environ.get('RIKO_DATA_DIR', Path(__file__).resolve().parents[1])) + os.environ.setdefault('RIKO_DATA_DIR', str(root)) # Where the backend client finds the API token. load_dotenv(root / '.env') # Never search parent/private directories for credentials. settings = DiscordAccess(root).settings() if not settings.token: raise ValueError('Discord_bot_token is required') if not settings.admins: raise ValueError('Configure Discord_admins before starting the bot') logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s') - CompanionBot(settings).run(settings.token) + bot = CompanionBot(settings) + if os.environ.get('RIKO_EXIT_WITH_BACKEND') == '1': + threading.Thread(target=exit_with_backend, args=(bot,), name='backend-watch', daemon=True).start() + bot.run(settings.token) if __name__ == '__main__': diff --git a/Code/process/app_core/desktop/api_guard.py b/Code/process/app_core/desktop/api_guard.py new file mode 100644 index 0000000..69714e1 --- /dev/null +++ b/Code/process/app_core/desktop/api_guard.py @@ -0,0 +1,145 @@ +"""Local API trust boundary: only Riko's own clients may use the backend. + +Every HTTP request and WebSocket handshake needs this backend start's token as +`Authorization: Bearer `, a loopback Host, and for sockets an app Origin. The +token and a separate confirmation key are fresh on every start. Packaged Electron generates +both, passes them in the environment, and sends the token only after the backend reports +that it holds its port. In development the backend mints them only after it holds its +port and writes them under persistent_memories (owner-only) for Electron to read, so a +token sent to a port squatter while the backend was down is useless afterwards. Electron main adds the token at the network layer, so renderer +JavaScript never sees it. Security-sensitive changes also need a single-use confirmation: +Electron main signs the server's challenge with the confirmation key, which never +travels over the network, only after the user approves a native dialog. +""" +import hashlib +import hmac +import json +import os +from pathlib import Path +import re +import secrets +import threading +import time + +HOSTS = {'127.0.0.1', 'localhost'} +# Packaged file:// renderer, the Vite dev server, and clients that send no Origin (Python, Node). +WEBSOCKET_ORIGINS = {'null', 'file://', 'http://localhost:5173', 'http://127.0.0.1:5173'} +SIGNATURE = re.compile(r'[0-9a-f]{64}') +SECRET = re.compile(r'[A-Za-z0-9_-]{32,256}') # what Electron and secrets.token_urlsafe produce + + +def secret_path(root, name): return Path(root) / 'persistent_memories' / name + + +def write_secret(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f'.{path.name}.{secrets.token_hex(4)}.tmp') + descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) # owner-only from the start + try: + with os.fdopen(descriptor, 'w', encoding='ascii') as stream: + stream.write(value) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + except BaseException: + temporary.unlink(missing_ok=True) + raise + + +def issue_secrets(root): + """Fresh (token, confirmation key) for this backend start; call once the port is bound. + + Values passed by packaged Electron are used as given. Both are removed from os.environ + so tool workers, MCP servers and other children never inherit them. + """ + token, key = os.environ.pop('RIKO_API_TOKEN', ''), os.environ.pop('RIKO_CONFIRM_KEY', '') + if SECRET.fullmatch(token) and SECRET.fullmatch(key) and token != key: return token, key + token, key = secrets.token_urlsafe(32), secrets.token_urlsafe(32) + write_secret(secret_path(root, 'api_token'), token) + write_secret(secret_path(root, 'confirm_key'), key) + return token, key + + +def client_root(): + """The data root a client process shares with the backend: the directory of RIKO_CONFIG.""" + base = Path(os.environ.get('RIKO_DATA_DIR') or Path.cwd()) + config = Path(os.environ.get('RIKO_CONFIG', 'character_config.yaml')).expanduser() + return (config if config.is_absolute() else base / config).parent + + +def client_token(): + """The current token for a client such as the Discord worker; read again on every use, + because it changes whenever the backend restarts.""" + if os.environ.get('RIKO_API_TOKEN'): return os.environ['RIKO_API_TOKEN'] + try: return secret_path(client_root(), 'api_token').read_text(encoding='ascii').strip() + except (OSError, ValueError): return '' + + +def signature(key, challenge): + return hmac.new(key.encode(), challenge.encode(), hashlib.sha256).hexdigest() + + +class Confirmations: + """Single-use, short-lived challenges for security-sensitive changes.""" + def __init__(self, ttl=120, limit=64): + self.ttl, self.limit = ttl, limit + self.pending = {} # nonce -> (challenge text, action, changes, expires) + self.lock = threading.Lock() + + def challenge(self, action, changes): + nonce, expires = secrets.token_urlsafe(16), int(time.time() + self.ttl) + text = json.dumps({'action': action, 'changes': changes, 'nonce': nonce, 'expires': expires}, + sort_keys=True, separators=(',', ':'), ensure_ascii=True) + with self.lock: + self._prune() + while len(self.pending) >= self.limit: self.pending.pop(next(iter(self.pending))) + self.pending[nonce] = (text, action, json.loads(json.dumps(changes)), expires) + return text + + def consume(self, key, action, changes, provided): + """True once for a valid signature over a pending challenge for exactly these changes.""" + if not isinstance(provided, str) or not SIGNATURE.fullmatch(provided): return False + changes = json.loads(json.dumps(changes)) + with self.lock: + self._prune() + for nonce, (text, pending_action, pending_changes, _) in list(self.pending.items()): + if pending_action == action and pending_changes == changes and hmac.compare_digest(signature(key, text), provided): + del self.pending[nonce] + return True + return False + + def _prune(self): + now = time.time() + for nonce in [n for n, item in self.pending.items() if item[3] < now]: del self.pending[nonce] + + +class LocalAPIGuard: + """Pure ASGI middleware, so it covers HTTP and WebSocket scopes alike.""" + def __init__(self, app, token): + self.app, self.token = app, token # token: zero-argument callable returning the current token + + async def __call__(self, scope, receive, send): + if scope['type'] not in ('http', 'websocket'): return await self.app(scope, receive, send) + headers = {key.decode('latin-1').lower(): value.decode('latin-1') for key, value in scope.get('headers', [])} + problem = self.problem(scope['type'], headers) + if problem is None: return await self.app(scope, receive, send) + status, detail = problem + if scope['type'] == 'websocket': + await receive() # websocket.connect; closing before accept rejects the handshake (HTTP 403) + await send({'type': 'websocket.close', 'code': 1008, 'reason': detail}) + return + body = json.dumps({'detail': detail}).encode() + await send({'type': 'http.response.start', 'status': status, + 'headers': [(b'content-type', b'application/json'), (b'content-length', str(len(body)).encode())]}) + await send({'type': 'http.response.body', 'body': body}) + + def problem(self, kind, headers): + host = headers.get('host', '') + if (host.split(':', 1)[0] if not host.startswith('[') else host).lower() not in HOSTS: + return 403, 'Requests must be addressed to 127.0.0.1 or localhost' + if kind == 'websocket' and headers.get('origin') not in (None, *WEBSOCKET_ORIGINS): + return 403, 'This origin may not open Riko sockets' + expected = 'Bearer ' + self.token() + if not hmac.compare_digest(headers.get('authorization', '').encode('latin-1'), expected.encode('latin-1')): + return 401, 'Missing or invalid API token' + return None diff --git a/Code/process/app_core/integrations/discord/backend.py b/Code/process/app_core/integrations/discord/backend.py index 62c12b7..dc4938f 100644 --- a/Code/process/app_core/integrations/discord/backend.py +++ b/Code/process/app_core/integrations/discord/backend.py @@ -10,6 +10,15 @@ logger = logging.getLogger(__name__) +def auth_headers(): + """This backend start's API token, read on every request and reconnect because it changes on + every backend restart. The launcher passes it as RIKO_API_TOKEN; a hand-started bot reads it + from the shared data root.""" + from ...desktop.api_guard import client_token + token = client_token() + return {'Authorization': 'Bearer ' + token} if token else {} + + class BackendError(RuntimeError): def __init__(self, status, detail): super().__init__(detail) @@ -39,11 +48,13 @@ async def open(self): async def request(self, method, path, *, body=None, data=None, binary=False): if self.http is None: raise BackendError(503, 'Backend connection is not open') - headers = {'Content-Type': 'application/octet-stream'} if data is not None else None + headers = {**auth_headers(), **({'Content-Type': 'application/octet-stream'} if data is not None else {})} try: async with self.http.request(method, self.base_url + path, json=body, data=data, headers=headers) as response: if response.status >= 400: detail = 'Companion backend unavailable; check the Python server.' + if response.status in {401, 403}: detail = 'The companion backend refused this client; restart Discord from the desktop app.' + if response.status == 428: detail = 'This change must be confirmed in the Riko desktop window.' if response.status in {400, 409, 413, 415, 422}: with contextlib.suppress(ValueError, aiohttp.ContentTypeError): detail = str((await response.json()).get('detail', detail))[:1000] @@ -61,7 +72,8 @@ async def request(self, method, path, *, body=None, data=None, binary=False): async def _events(self): while not self.closed: try: - async with self.http.ws_connect(self.base_url + '/ws/discord/client?instance=' + self.instance, max_msg_size=4 * 1024 * 1024) as socket: + async with self.http.ws_connect(self.base_url + '/ws/discord/client?instance=' + self.instance, max_msg_size=4 * 1024 * 1024, + headers=auth_headers()) as socket: self.socket = socket async for message in socket: if message.type != aiohttp.WSMsgType.TEXT: continue diff --git a/Code/process/app_core/integrations/discord/commands.py b/Code/process/app_core/integrations/discord/commands.py index b531f35..a0f0a90 100644 --- a/Code/process/app_core/integrations/discord/commands.py +++ b/Code/process/app_core/integrations/discord/commands.py @@ -112,6 +112,8 @@ async def settings(self, interaction: discord.Interaction, key: str | None = Non @app_commands.command(name='tool_policy', description='Require or remove approval for one exact registered tool name.') async def tool_policy(self, interaction: discord.Interaction, name: str, required: bool): self.require(interaction, admin=True) + # Removing approval lets the model run that tool unattended; only the desktop owner can confirm it. + if not required: raise ValueError('Removing approval must be confirmed in the Riko desktop window (Settings).') await interaction.response.defer(ephemeral=True) await self.bot.backend.request('PUT', '/api/tools/approvals', body={'policy': {name: required}}) await self.reply(interaction, f'Approval requirement for {name}: {required}.') diff --git a/Code/process/app_core/integrations/discord/config.py b/Code/process/app_core/integrations/discord/config.py index 7dc29f0..42c29d4 100644 --- a/Code/process/app_core/integrations/discord/config.py +++ b/Code/process/app_core/integrations/discord/config.py @@ -28,8 +28,10 @@ class BotSettings: def from_env(cls, root, env): backend = env.get('Discord_backend_url', 'http://127.0.0.1:8765').rstrip('/') parsed = urlsplit(backend) - if parsed.scheme != 'http' or parsed.hostname not in {'127.0.0.1', 'localhost', '::1'} or parsed.username or parsed.password or parsed.path or parsed.query or parsed.fragment: - raise ValueError('Discord backend must be a loopback http:// URL without credentials or a path') + if parsed.scheme != 'http' or parsed.hostname not in {'127.0.0.1', 'localhost'} or parsed.username or parsed.password or parsed.path or parsed.query or parsed.fragment: + raise ValueError('Discord backend must be http://127.0.0.1: without credentials or a path') + # Always 127.0.0.1: 'localhost' can resolve to ::1, where another account's listener would receive the API token. + backend = 'http://127.0.0.1' + (f':{parsed.port}' if parsed.port else '') camera = env.get('Discord_camera_url', '').strip() if camera: url = urlsplit(camera) diff --git a/Code/process/app_core/integrations/discord/launcher.py b/Code/process/app_core/integrations/discord/launcher.py index 34a18d7..4b198e3 100644 --- a/Code/process/app_core/integrations/discord/launcher.py +++ b/Code/process/app_core/integrations/discord/launcher.py @@ -16,6 +16,7 @@ class DiscordLauncher: def __init__(self, root, state=None): self.root = Path(root) self.lock = threading.RLock() + self.token = lambda: '' # The backend's API token accessor; desktop_server sets it. self.outbox = Outbox(event_bus) # status/notifications reach the session; never emit under self.lock self.process = None self.error = '' @@ -134,8 +135,11 @@ def _start(self): script = self.root / 'Code' / 'discord_bot.py' if not getattr(sys, 'frozen', False) and not script.is_file(): raise ValueError('Discord client entry point is missing') command = [sys.executable, '--discord-worker'] if getattr(sys, 'frozen', False) else [sys.executable, str(script)] - self.process = subprocess.Popen(command, cwd=self.root, - stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + # The worker exits when its stdin closes, i.e. when this backend exits however it exits, + # so a client holding a dead backend's token never outlives it. + env = {**os.environ, 'RIKO_API_TOKEN': self.token(), 'RIKO_DATA_DIR': str(self.root), 'RIKO_EXIT_WITH_BACKEND': '1'} + self.process = subprocess.Popen(command, cwd=self.root, env=env, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) self.error = '' except ValueError as exc: @@ -163,6 +167,9 @@ def _watch(self, process): with self.lock: if self.process is process: self.error = errors[code] code = process.wait() + if getattr(process, 'stdin', None): + try: process.stdin.close() + except OSError: pass with self.lock: if self.process is not process: return self.process = None diff --git a/Code/run_server.py b/Code/run_server.py index 6938fec..3305ddd 100644 --- a/Code/run_server.py +++ b/Code/run_server.py @@ -37,6 +37,24 @@ def main(): # voice.level/chat.delta packet and can dominate the capture/UI event loop. print("Riko AI server: http://127.0.0.1:8765 — Ctrl+C to stop", flush=True) import desktop_server + # Hold the port before the model loads, so no other process (or account) can listen on it and + # collect the API token Electron sends. In development this start's token is minted only now, so a + # squatter could only have seen the previous one; packaged Electron sends its token only after the + # line below. Requests queue until startup finishes. + import socket + import time + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE if os.name == 'nt' else socket.SO_REUSEADDR, 1) + deadline = time.monotonic() + 20 # a previous backend may still be shutting down after a quick relaunch + while True: + try: listener.bind(('127.0.0.1', 8765)); break + except OSError as exc: + if time.monotonic() >= deadline: + raise SystemExit(f'Port 8765 is already in use ({exc}); stop the other process and try again.') from None + time.sleep(.5) + listener.listen(2048) + desktop_server.api_secrets() + if os.environ.get('RIKO_MANAGED') == '1': print('RIKO_BACKEND_LISTENING', flush=True) # see electron/release.cjs server = uvicorn.Server(uvicorn.Config(desktop_server.app, host="127.0.0.1", port=8765, log_level="info", timeout_graceful_shutdown=10)) if os.environ.get('RIKO_MANAGED') == '1': @@ -46,7 +64,7 @@ def managed_shutdown(): if line.strip() == 'shutdown': break server.should_exit = True threading.Thread(target=managed_shutdown, name='release-shutdown', daemon=True).start() - server.run() + server.run(sockets=[listener]) if __name__ == "__main__": diff --git a/electron/main.cjs b/electron/main.cjs index 02fcc01..205ba9e 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,4 +1,4 @@ -const { app, BrowserWindow, globalShortcut, ipcMain, dialog, screen, shell, Tray, Menu, nativeImage } = require('electron'); +const { app, BrowserWindow, globalShortcut, ipcMain, dialog, screen, session, shell, Tray, Menu, nativeImage } = require('electron'); // Hardware acceleration is Electron's default; keep WebGL and GPU rasterization enabled. app.commandLine.appendSwitch('enable-gpu-rasterization'); const path = require('path'); @@ -40,7 +40,33 @@ const {windowAction} = require('./window_actions.cjs'); let boardScreen = 0, geometryTimer; let displaySignature = '', displayPublishing = false; const API = 'http://127.0.0.1:8765'; -const patchBoard = body => fetch(API + '/api/surfaces/whiteboard', {method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)}).catch(() => {}); +// The backend requires the install's API token (Code/process/app_core/desktop/api_guard.py). Main adds it +// to every request to the backend at the network layer, so renderer JavaScript never sees it. +// Both secrets are fresh on every backend start. Packaged builds generate them here, hand them to the +// backend they spawn (no files) and send the token only once it reports that it holds the port; in +// development the backend writes them beside its config after it holds the port, and main re-reads them +// whenever the files change. +let secretDirectory = '', ownSecrets = null; +const secretCache = {}; +function backendSecret(name) { + if (ownSecrets) return ownSecrets.listening ? ownSecrets[name] : ''; + if (!secretDirectory) return ''; + const file = path.join(secretDirectory, name), cached = secretCache[name] || {mtime: 0, value: ''}; + try { const stat = fs.statSync(file); if (stat.mtimeMs !== cached.mtime) secretCache[name] = {mtime: stat.mtimeMs, value: fs.readFileSync(file, 'ascii').trim()}; } + catch { secretCache[name] = {mtime: 0, value: ''}; } + return secretCache[name].value; +} +const apiToken = () => backendSecret('api_token'); +function injectApiToken() { + // Only 127.0.0.1: 'localhost' may resolve to ::1, where another account could listen. + session.defaultSession.webRequest.onBeforeSendHeaders({urls: ['http://127.0.0.1:8765/*', 'ws://127.0.0.1:8765/*']}, (details, callback) => { + const token = apiToken(); + if (token && new URL(details.url).host === '127.0.0.1:8765') details.requestHeaders.Authorization = 'Bearer ' + token; + callback({requestHeaders: details.requestHeaders}); + }); +} +const backendFetch = (route, init = {}) => fetch(API + route, {...init, headers: {...init.headers, Authorization: 'Bearer ' + apiToken()}}); +const patchBoard = body => backendFetch('/api/surfaces/whiteboard', {method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)}).catch(() => {}); const dev = process.env.RIKO_DEV === '1'; function page(name) { return dev ? `http://localhost:5173/#/${name}` : `file://${path.join(__dirname, 'dist', 'index.html')}#/${name}`; } function displayList() { @@ -53,7 +79,7 @@ async function publishDisplays() { if (displayPublishing || signature === displaySignature) return; displayPublishing = true; try { - const response = await fetch(API + '/api/displays', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: signature}); + const response = await backendFetch('/api/displays', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: signature}); if (response.ok) displaySignature = signature; } catch { /* Retry on the next backend snapshot/reconnection. */ } finally {displayPublishing = false;} @@ -156,7 +182,7 @@ function createTray(characterName){ const icon=nativeImage.createFromPath(brandIcon); if(icon.isEmpty())throw new Error('Tray icon is missing or invalid: assets/tray.png'); tray=new Tray(icon.resize({width:24,height:24}));tray.setToolTip(characterName||'Companion'); - tray.setContextMenu(Menu.buildFromTemplate([{label:'Open chat',click:()=>showControls('chat')},{label:'Start Discord client',click:async()=>{try{const response=await fetch(API+'/api/discord/start',{method:'POST'});if(!response.ok){let message='Discord could not be started';try{const body=await response.json();if(typeof body.detail==='string')message=body.detail;}catch{}dialog.showErrorBox('Discord',message);}}catch{dialog.showErrorBox('Discord','The Python backend is not available. Start it before launching Discord.');}}},{label:'Settings',click:()=>showControls('settings')},{label:'Appearance',click:()=>showControls('appearance')},{label:'Whiteboard',click:()=>{patchBoard({visible:true});setBoardMode('full');}},{type:'separator'},{label:'Quit',click:()=>app.quit()}])); + tray.setContextMenu(Menu.buildFromTemplate([{label:'Open chat',click:()=>showControls('chat')},{label:'Start Discord client',click:async()=>{try{const response=await backendFetch('/api/discord/start',{method:'POST'});if(!response.ok){let message='Discord could not be started';try{const body=await response.json();if(typeof body.detail==='string')message=body.detail;}catch{}dialog.showErrorBox('Discord',message);}}catch{dialog.showErrorBox('Discord','The Python backend is not available. Start it before launching Discord.');}}},{label:'Settings',click:()=>showControls('settings')},{label:'Appearance',click:()=>showControls('appearance')},{label:'Whiteboard',click:()=>{patchBoard({visible:true});setBoardMode('full');}},{type:'separator'},{label:'Quit',click:()=>app.quit()}])); tray.on('double-click',()=>showControls('chat')); } app.whenReady().then(async () => { @@ -169,11 +195,19 @@ app.whenReady().then(async () => { } root=JSON.parse(fs.readFileSync(locator,'utf8')).directory; process.env.RIKO_CONFIG=path.join(root,'character_config.yaml'); - backendProcess=release.startBackend(root,process.resourcesPath); + const crypto=require('crypto'); + ownSecrets={api_token:crypto.randomBytes(32).toString('base64url'),confirm_key:crypto.randomBytes(32).toString('base64url'),listening:false}; + // Nothing carries the token until the backend says it holds the port, and nothing does after it exits. + backendProcess=release.startBackend(root,process.resourcesPath,ownSecrets,()=>{ownSecrets.listening=true;publishProcesses(true);}); backendProcess.on('error',error=>dialog.showErrorBox('Backend failed to launch',error.message)); - backendProcess.on('exit',code=>{if(!app.isQuitting&&code)dialog.showErrorBox('Backend stopped','Review logs/backend-launch.log in your data folder. Open Settings to correct the model or backend configuration.');}); + backendProcess.on('exit',code=>{ownSecrets.listening=false;if(!app.isQuitting&&code)dialog.showErrorBox('Backend stopped','Review logs/backend-launch.log in your data folder. Open Settings to correct the model or backend configuration.');}); } - const configPath = path.resolve(root, process.env.RIKO_CONFIG || 'character_config.yaml'); + // Resolve the config as run_server does (relative to RIKO_DATA_DIR in development), so main reads the + // secrets the backend wrote beside it. + const configPath = path.resolve(!app.isPackaged && process.env.RIKO_DATA_DIR ? process.env.RIKO_DATA_DIR : root, process.env.RIKO_CONFIG || 'character_config.yaml'); + // In development the backend writes its secrets beside the config it loads; inject before any window loads. + secretDirectory = path.join(path.dirname(configPath), 'persistent_memories'); + injectApiToken(); const config = YAML.parse(fs.readFileSync(configPath, 'utf8')) || {}; if(app.isPackaged){ try{sovitsProcess=release.startSovits(config.sovits_ping_config);sovitsProcess?.on('error',error=>dialog.showErrorBox('GPT-SoVITS could not start',error.message));} @@ -189,7 +223,7 @@ app.whenReady().then(async () => { const shortcuts=config.desktop?.shortcuts||{}; const actions={popup:()=>showControls(),quit:()=>app.quit(),whiteboard:()=>patchBoard({visible:!whiteboard.isVisible()}), settings:()=>showControls('settings'), - mic:()=>fetch(API+'/api/mic/toggle',{method:'POST'}).catch(()=>{}),audio:()=>fetch(API+'/api/audio/toggle',{method:'POST'}).catch(()=>{}),sleep:()=>fetch(API+'/api/sleep/toggle',{method:'POST'}).catch(()=>{})}; + mic:()=>backendFetch('/api/mic/toggle',{method:'POST'}).catch(()=>{}),audio:()=>backendFetch('/api/audio/toggle',{method:'POST'}).catch(()=>{}),sleep:()=>backendFetch('/api/sleep/toggle',{method:'POST'}).catch(()=>{})}; const defaults={popup:'CommandOrControl+Shift+Space',quit:'CommandOrControl+Shift+Q',whiteboard:'CommandOrControl+Shift+W',settings:'CommandOrControl+Shift+,'}; for(const [name,action] of Object.entries(actions)){ const accelerator=shortcuts[name]??defaults[name]; @@ -205,7 +239,7 @@ function publishProcesses(force=false,empty=false){ if(!app.isReady())return; const body=JSON.stringify({processes:empty?[]:app.getAppMetrics().map(item=>({pid:item.pid,kind:['Browser','GPU','Renderer','Utility','Zygote','Sandbox helper'].includes(item.type)?item.type:'Utility'}))}); if(!force&&body===processSignature)return;processSignature=body; - processUpdates=processUpdates.then(()=>fetch(API+'/api/resources/electron',{method:'POST',headers:{'Content-Type':'application/json'},body})).catch(()=>{processSignature='';}); + processUpdates=processUpdates.then(()=>backendFetch('/api/resources/electron',{method:'POST',headers:{'Content-Type':'application/json'},body})).catch(()=>{processSignature='';}); } app.whenReady().then(()=>publishProcesses()); app.on('web-contents-created',(_event,contents)=>{contents.on('did-finish-load',()=>publishProcesses());contents.on('destroyed',()=>publishProcesses());contents.on('render-process-gone',()=>publishProcesses());}); @@ -257,6 +291,23 @@ ipcMain.handle('capture-whiteboard', async (event, rect) => { return image.toPNG(); }); ipcMain.handle('displays', () => displayList()); +// Security-sensitive changes (native library, programs Riko starts, unattended tools, Discord access) need +// the user's approval here. The signature uses the confirmation key, which never leaves main and the backend, +// over the exact change the backend asked to confirm, so a compromised page cannot approve it silently. +ipcMain.handle('confirm-security-change', async (event, challenge) => { + if (!control || control.isDestroyed() || event.sender.id !== control.webContents.id) throw new Error('Settings window required'); + let change; + try { change = JSON.parse(challenge); } catch { throw new Error('Invalid confirmation request'); } + const titles = {settings: 'Change security-sensitive settings?', tool_approvals: 'Let these tools run without asking first?', discord_access: 'Change who can use Riko from Discord?'}; + // Show control, invisible and direction-changing characters as escapes, so the text shown is what gets signed. + const visible = text => String(text).replace(/[\p{C}\p{Zl}\p{Zp}]/gu, c => `\\u{${c.codePointAt(0).toString(16)}}`); + const lines = Object.entries(change.changes || {}).map(([key, value]) => `${visible(key)}: ${visible(JSON.stringify(value))}`); + const {response} = await dialog.showMessageBox(control, {type: 'warning', buttons: ['Cancel', 'Allow'], defaultId: 0, cancelId: 0, noLink: true, + message: titles[change.action] || 'Confirm this change?', detail: lines.join('\n') + '\n\nOnly allow this if you made this change yourself.'}); + const key = backendSecret('confirm_key'); + if (response !== 1 || !key) return null; + return require('crypto').createHmac('sha256', key).update(challenge).digest('hex'); +}); ipcMain.handle('pick-path', async (event, options={}) => { if(!control||event.sender.id!==control.webContents.id)throw new Error('Path selection is limited to controls'); const result=await dialog.showOpenDialog(control,{title:options.directory?'Choose a folder':'Choose a file', diff --git a/electron/preload.cjs b/electron/preload.cjs index 3753fd9..a03e01c 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -15,4 +15,4 @@ contextBridge.exposeInMainWorld('controlVisibilityBridge', {get:()=>ipcRenderer. contextBridge.exposeInMainWorld('whiteboardBridge', {capture: rect => ipcRenderer.invoke('capture-whiteboard', rect)}); contextBridge.exposeInMainWorld('processBridge', {sync:()=>ipcRenderer.send('sync-processes')}); contextBridge.exposeInMainWorld('approvalBridge', {interactive: enabled => ipcRenderer.send('approval-interactive', enabled)}); -contextBridge.exposeInMainWorld('riko', {showControl: () => ipcRenderer.send('show-control'), showWhiteboard: () => ipcRenderer.send('show-whiteboard'), syncSurfaces: state => ipcRenderer.send('sync-surfaces', state), displays: () => ipcRenderer.invoke('displays'), avatarInteractive: enabled => ipcRenderer.send('avatar-interactive', enabled), pickPath: options => ipcRenderer.invoke('pick-path', options), onNavigate: callback => {const listener=(_event,view)=>callback(view);ipcRenderer.on('navigate',listener);return()=>ipcRenderer.removeListener('navigate',listener);}}); +contextBridge.exposeInMainWorld('riko', {showControl: () => ipcRenderer.send('show-control'), showWhiteboard: () => ipcRenderer.send('show-whiteboard'), syncSurfaces: state => ipcRenderer.send('sync-surfaces', state), displays: () => ipcRenderer.invoke('displays'), avatarInteractive: enabled => ipcRenderer.send('avatar-interactive', enabled), pickPath: options => ipcRenderer.invoke('pick-path', options), confirmChange: challenge => ipcRenderer.invoke('confirm-security-change', challenge), onNavigate: callback => {const listener=(_event,view)=>callback(view);ipcRenderer.on('navigate',listener);return()=>ipcRenderer.removeListener('navigate',listener);}}); diff --git a/electron/release.cjs b/electron/release.cjs index 364edd7..3353b1b 100644 --- a/electron/release.cjs +++ b/electron/release.cjs @@ -38,13 +38,30 @@ function saveSetup(directory,input,resources){ fs.writeFileSync(path.join(directory,'character_config.yaml'),YAML.stringify(config),{flag:'wx',mode:0o600}); return directory; } -function startBackend(directory,resources){ +// The backend prints this line once it holds 127.0.0.1:8765. Main sends the API token only after it, so a +// process that grabbed the port while the backend was still starting never sees this start's token. +const LISTENING='RIKO_BACKEND_LISTENING'; +function watchListening(onListening){ + let tail=''; + return chunk=>{ + if(tail===null)return; + tail=(tail+String(chunk)).slice(-4096); + if(tail.split(/\r?\n/).slice(0,-1).includes(LISTENING)){tail=null;onListening();} + }; +} +function startBackend(directory,resources,secrets={},onListening=()=>{}){ const executable=path.join(resources,'backend',process.platform==='win32'?'riko-backend.exe':'riko-backend'); - const log=fs.openSync(path.join(directory,'logs','backend-launch.log'),'a'); - const env={...process.env,RIKO_MANAGED:'1',RIKO_DATA_DIR:directory,RIKO_CONFIG:path.join(directory,'character_config.yaml'),HF_HOME:path.join(directory,'models','huggingface'),TORCH_HOME:path.join(directory,'models','torch'),XDG_CACHE_HOME:path.join(directory,'models','cache'),RIKO_BUNDLE_ROOT:resources}; + const logPath=path.join(directory,'logs','backend-launch.log'); + const log=fs.openSync(logPath,'a'); + // This start's API token and confirmation key go to the backend only; it removes them from its own environment. + const own=secrets.api_token&&secrets.confirm_key?{RIKO_API_TOKEN:secrets.api_token,RIKO_CONFIRM_KEY:secrets.confirm_key}:{}; + const env={...process.env,...own,RIKO_MANAGED:'1',RIKO_DATA_DIR:directory,RIKO_CONFIG:path.join(directory,'character_config.yaml'),HF_HOME:path.join(directory,'models','huggingface'),TORCH_HOME:path.join(directory,'models','torch'),XDG_CACHE_HOME:path.join(directory,'models','cache'),RIKO_BUNDLE_ROOT:resources}; let child; - try{child=spawn(executable,[],{cwd:directory,env,stdio:['pipe',log,log],windowsHide:true});}finally{fs.closeSync(log);} + try{child=spawn(executable,[],{cwd:directory,env,stdio:['pipe','pipe',log],windowsHide:true});}finally{fs.closeSync(log);} child.stdin.on('error',()=>{}); + const output=fs.createWriteStream(logPath,{flags:'a'}),listening=watchListening(onListening); + child.stdout.on('data',chunk=>{output.write(chunk);listening(chunk);}); + child.stdout.on('end',()=>output.end()); return child; } function startSovits(settings){ @@ -55,4 +72,4 @@ function startSovits(settings){ return spawn(executable,args,{cwd:path.dirname(executable),stdio:'ignore',windowsHide:true,shell:false}); } function stopBackend(child){return new Promise(resolve=>{if(!child||child.exitCode!==null){resolve();return;}const timeout=setTimeout(()=>{child.kill();resolve();},15000);child.once('exit',()=>{clearTimeout(timeout);resolve();});child.stdin.end('shutdown\n');});} -module.exports={hardware,configuration,saveSetup,startBackend,nativeLibrary,startSovits,stopBackend}; +module.exports={hardware,configuration,saveSetup,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,LISTENING}; diff --git a/electron/src/api.mjs b/electron/src/api.mjs index 2c63148..3a27ed6 100644 --- a/electron/src/api.mjs +++ b/electron/src/api.mjs @@ -2,16 +2,28 @@ export const API = 'http://127.0.0.1:8765'; export const EVENTS = 'ws://127.0.0.1:8765/ws/events'; export const mediaURL = path => API + '/api/media?path=' + encodeURIComponent(path); -export async function request(path, {method = 'GET', body, signal} = {}) { +// Electron main adds the API token to every backend request; renderer code never handles it. +export async function request(path, {method = 'GET', body, signal} = {}, confirmation) { + const headers = {...(body === undefined ? {} : {'Content-Type': 'application/json'}), ...(confirmation ? {'X-Riko-Confirmation': confirmation} : {})}; const response = await fetch(API + path, { method, ...(signal?{signal}:{}), - ...(body === undefined ? {} : {headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)}), + headers, + ...(body === undefined ? {} : {body: JSON.stringify(body)}), }); + if (response.status === 428 && !confirmation) { + // A security-sensitive change: Electron main asks the user and signs the exact change if they allow it. + let challenge; try {challenge=(await response.clone().json()).detail?.confirm;} catch {} + if (challenge) { + const signature = await globalThis.riko?.confirmChange?.(challenge); + if (!signature) throw new Error('Change cancelled. Nothing was saved.'); + return request(path, {method, body, signal}, signature); + } + } if (!response.ok) { const text=await response.text(); let message=text; - try {const value=JSON.parse(text);if(typeof value.detail==='string')message=value.detail;}catch{} + try {const value=JSON.parse(text);if(typeof value.detail==='string')message=value.detail;else if(typeof value.detail?.detail==='string')message=value.detail.detail;}catch{} if(response.status===404&&path.startsWith('/api/discord/'))message='This Python backend does not support the current Discord controls. Restart Python from this updated checkout, then retry.'; throw new Error(message); } diff --git a/electron/src/release.test.mjs b/electron/src/release.test.mjs index 58ab5ac..1470d20 100644 --- a/electron/src/release.test.mjs +++ b/electron/src/release.test.mjs @@ -5,9 +5,10 @@ import os from 'node:os'; import path from 'node:path'; import {createRequire} from 'node:module'; const require=createRequire(import.meta.url); -const {configuration,saveSetup,startSovits}=require('../release.cjs'); +const {configuration,saveSetup,startSovits,watchListening,LISTENING}=require('../release.cjs'); const YAML=require('yaml'); function fixture(){const root=fs.mkdtempSync(path.join(os.tmpdir(),'riko-release-'));const folder=path.join(root,'native','cuda');fs.mkdirSync(folder,{recursive:true});fs.writeFileSync(path.join(folder,process.platform==='win32'?'riko-native.dll':'libriko-native.so'),'test');return root;} const form={backend:'cuda',repo:'owner/model',filename:'model.gguf',context:8192,output:1024,threads:4,memories:'I like tea.\nMy name is Alex.',sovitsAuto:false}; test('setup separates persistent data from binaries and refuses overwrite',()=>{const root=fixture();const directory=fs.mkdtempSync(path.join(os.tmpdir(),'riko-data-'));try{saveSetup(directory,form,root);const config=YAML.parse(fs.readFileSync(path.join(directory,'character_config.yaml'),'utf8'));assert.equal(config.runtime.native_library,'bundled:cuda');assert.equal(config.memory.default_memories.length,2);assert.equal(config.tools.require_approval,true);assert.equal(config.emotion.probe.enabled,false);assert.ok(fs.existsSync(path.join(directory,'models')));assert.throws(()=>saveSetup(directory,form,root),/EEXIST/);assert.throws(()=>saveSetup(root,form,root),/outside/);}finally{fs.rmSync(root,{recursive:true,force:true});fs.rmSync(directory,{recursive:true,force:true});}}); test('setup validates budgets, model and explicit speech consent',()=>{const root=fixture();try{assert.throws(()=>configuration({...form,output:8192},root),/budget/);assert.throws(()=>configuration({...form,filename:'../secret.gguf'},root),/GGUF/);assert.throws(()=>configuration({...form,sovitsAuto:true},root),/GPT-SoVITS/);assert.equal(startSovits({auto_start:false}),null);assert.throws(()=>startSovits({auto_start:true,executable:'relative.exe'}),/missing/);}finally{fs.rmSync(root,{recursive:true,force:true});}}); +test('main learns the backend holds its port only from a complete marker line',()=>{let calls=0;const watch=watchListening(()=>calls++);watch('Riko AI server: http://127.0.0.1:8765\nRIKO_BACKEND_');assert.equal(calls,0);watch('LISTENING_NOT\n');assert.equal(calls,0);watch(LISTENING+'\r\nINFO later\n');assert.equal(calls,1);watch(LISTENING+'\n');assert.equal(calls,1);const split=watchListening(()=>calls++);for(const part of ['RIKO_BACK','END_LISTEN','ING','\n'])split(Buffer.from(part));assert.equal(calls,2);}); diff --git a/tests/test_desktop_api.py b/tests/test_desktop_api.py index b793092..2c23339 100644 --- a/tests/test_desktop_api.py +++ b/tests/test_desktop_api.py @@ -1,5 +1,6 @@ from types import SimpleNamespace import importlib +import os import pytest from fastapi.testclient import TestClient @@ -7,6 +8,11 @@ from process.app_core.desktop.state import DesktopState +def client_for(backend, **options): + """A client that passes the local API guard: loopback Host plus this test data root's token.""" + return TestClient(backend.app, base_url='http://127.0.0.1:8765', headers={'Authorization': 'Bearer ' + backend.api_token()}, **options) + + @pytest.fixture def backend(monkeypatch, tmp_path): # Load settings with the real loader before the server-import startup stub. @@ -36,7 +42,7 @@ def close(self): calls.append('close') monkeypatch.setattr(backend, 'Initiative', lambda session: None) before = len(backend.event_bus._listeners) assert calls == [] - with TestClient(backend.app) as client: + with client_for(backend) as client: assert calls == ['create'] assert client.get('/api/status').status_code == 200 assert len(backend.state._listeners) == 1 @@ -55,7 +61,7 @@ def test_animation_assets_and_results_are_scoped(backend, monkeypatch): outside.touch() actions = ActionController() monkeypatch.setattr(backend.session, 'actions', actions, raising=False) - client = TestClient(backend.app) + client = client_for(backend) try: assert client.get('/api/avatar/animation', params={'path': 'character_files/wake.vrma'}).content == b'test animation' assert client.get('/api/avatar/animation', params={'path': 'outside.vrma'}).status_code == 400 @@ -70,7 +76,7 @@ def test_animation_assets_and_results_are_scoped(backend, monkeypatch): def test_audio_volume_is_live_bounded_and_independent_of_mute(backend): - client = TestClient(backend.app) + client = client_for(backend) backend.state.toggle_audio() result = client.patch('/api/audio/volume', json={'volume': .35}) assert result.status_code == 200 @@ -87,7 +93,7 @@ def test_audio_volume_is_live_bounded_and_independent_of_mute(backend): def test_discord_start_is_explicit_and_runtime_scoped(backend, monkeypatch): calls = [] monkeypatch.setattr(backend, 'discord_launcher', SimpleNamespace(status=lambda: {'running': False, 'error': ''}, start=lambda: calls.append('start') or {'running': True, 'error': ''})) - client = TestClient(backend.app) + client = client_for(backend) assert client.get('/api/discord/process').json()['running'] is False assert calls == [] assert client.post('/api/discord/start').json()['running'] is True @@ -106,7 +112,7 @@ def test_avatar_library_import_and_settings_apply_live(backend, monkeypatch): monkeypatch.setattr(backend, 'settings_store', lambda: store) monkeypatch.setattr(backend, 'load_config', load_config) source = backend.config.root/'source.vrm'; source.write_bytes(vrm_bytes()) - client = TestClient(backend.app) + client = client_for(backend) imported = client.post('/api/avatar/models/import',json={'value':str(source)}) assert imported.status_code == 200 model = imported.json()['path'] @@ -140,7 +146,7 @@ def test_background_pause_setting_applies_live_without_restart(backend,monkeypat memory=SimpleNamespace(set_foreground=lambda value:calls.append(('memory',value))) monkeypatch.setattr(backend,'chat',SimpleNamespace(provider=provider,memory_store=memory)) monkeypatch.setattr(backend.session,'_generation_active',True,raising=False) - result=TestClient(backend.app).put('/api/settings',json={'revision':store.snapshot()['revision'],'changes':{'runtime.pause_background_on_live':False}}) + result=client_for(backend).put('/api/settings',json={'revision':store.snapshot()['revision'],'changes':{'runtime.pause_background_on_live':False}}) assert result.status_code==200 and result.json()['saved'] and not result.json()['restart_required'] assert calls==[('pause',False),('memory',False)] @@ -155,7 +161,7 @@ def test_gpu_draft_feedback_can_estimate_budget_mismatch_without_fixing_values(b monkeypatch.setattr(backend,'load_config',load_config) monkeypatch.setattr(backend.gpu_monitor,'sample',lambda *args:{}) monkeypatch.setattr('process.app_core.resources.vram_estimate.estimate',lambda config,telemetry:{'n_ctx':config.runtime.n_ctx,'warnings':[]}) - client=TestClient(backend.app) + client=client_for(backend) result=client.post('/api/resources/estimate',json={'changes':{'runtime.n_ctx':2048}}) assert result.status_code==200 and result.json()['estimate']['n_ctx']==2048 assert result.json()['validation_errors'] and result.json()['estimate']['warnings'] @@ -176,7 +182,7 @@ def test_animation_library_api_import_assignment_preview_and_interaction(backend monkeypatch.setattr(backend, 'session', session) source = backend.config.root / 'source.pose.json' source.write_text(json.dumps({'version': 1, 'bones': {'head': [0, .1, 0]}})) - client = TestClient(backend.app) + client = client_for(backend) try: assert client.post('/api/animation/capabilities', json={'bones': ['head'], 'expressions': []}).status_code == 200 response = client.post('/api/animation/import', json={'path': str(source), 'metadata': {'states': ['idle']}}) @@ -204,7 +210,7 @@ def test_resource_estimate_previews_drafts_without_saving(backend, monkeypatch): before = path.read_bytes() monkeypatch.setattr(backend, 'load_config', load_config) monkeypatch.setattr(backend.gpu_monitor, 'sample', lambda *args: {'available': False, 'gpus': []}) - client = TestClient(backend.app) + client = client_for(backend) response = client.post('/api/resources/estimate', json={'changes': {'memory.reflection_context_window_tokens': 8192}}) assert response.status_code == 200 assert response.json()['estimate']['kv']['reflection_context_tokens'] == 8192 @@ -221,7 +227,7 @@ def test_tool_approval_api_policy_and_decision(backend, monkeypatch, tmp_path): registry.tools['example'] = RegisteredTool('example', 'Test tool', {}, lambda args: 'ok') registry.approvals = ToolApprovals(tmp_path / 'approvals.json') monkeypatch.setattr(backend, 'chat', SimpleNamespace(tool_registry=registry)) - client = TestClient(backend.app) + client = client_for(backend) try: assert client.get('/api/tools/approvals').json()['tools'][0]['name'] == 'example' assert client.put('/api/tools/approvals', json={'policy': {'example': True}}).status_code == 200 @@ -238,9 +244,9 @@ def test_resource_websocket_bootstrap_and_source_triggered_updates(backend, monk bridge = ResourceEvents(bus, {'approvals': lambda: {'pending': list(pending)}}) monkeypatch.setattr(backend, 'event_bus', bus) monkeypatch.setattr(backend, 'resource_events', bridge) - client = TestClient(backend.app) + client = client_for(backend) try: - with client.websocket_connect('/ws/events') as socket: + with client.websocket_connect('ws://127.0.0.1:8765/ws/events') as socket: assert socket.receive_json()['type'] == 'state.snapshot' assert socket.receive_json()['payload']['approvals']['pending'] == [] pending.append({'id':'request'}) @@ -260,14 +266,14 @@ def test_corrupt_saved_desktop_settings_do_not_abort_startup(backend, monkeypatc monkeypatch.setattr(backend, 'SessionManager', lambda *args: SimpleNamespace( runtime_snapshot=lambda: {'runtime': {}}, close=lambda: None)) monkeypatch.setattr(backend, 'Initiative', lambda session: None) - with TestClient(backend.app): + with client_for(backend): assert backend.state.avatar_geometry['width'] == 480 assert not backend._desktop_settings_loaded assert path.read_bytes() == original def test_surface_api_rejects_empty_bounds_and_noninteger_geometry(backend): - client = TestClient(backend.app) + client = client_for(backend) command = backend.state.add_whiteboard('text', {'text': 'hello'}) response = client.post('/api/surfaces/result', json={ 'surface': 'whiteboard', 'command_id': command, 'status': 'rendered', 'bounds': {}}) @@ -283,7 +289,7 @@ def test_failed_session_start_closes_constructed_chat(backend,monkeypatch): def fail(*args): raise RuntimeError('session startup failed') monkeypatch.setattr(backend,'SessionManager',fail) with pytest.raises(RuntimeError,match='session startup failed'): - with TestClient(backend.app): pass + with client_for(backend): pass assert closed==['chat'] @@ -292,7 +298,7 @@ def test_setup_mode_keeps_settings_accessible_with_cors(backend,monkeypatch): (backend.config.root/'character_config.yaml').write_text('runtime:\n provider: lm_studio\n') def fail(config): raise RuntimeError('llama-server missing') monkeypatch.setattr(backend,'create_chat_service',fail) - with TestClient(backend.app) as client: + with client_for(backend) as client: assert client.get('/api/settings').status_code==200 assert client.get('/api/status').json()['startup_error']=='llama-server missing' response=client.get('/api/voice/status',headers={'Origin':'http://127.0.0.1:5173'}) @@ -305,7 +311,7 @@ def test_history_api_is_paginated_without_replaying_events(backend,monkeypatch): from process.app_core.conversation.messages import ChatMessage store=ConversationStore(backend.config.root/'history.sqlite3',legacy=[ChatMessage('user',str(i)) for i in range(10)]) monkeypatch.setattr(backend,'conversation_store',store) - client=TestClient(backend.app) + client=client_for(backend) try: page=client.get('/api/chat/history?limit=3').json() assert [m['text'] for m in page['messages']]==['7','8','9'] @@ -316,7 +322,7 @@ def test_history_api_is_paginated_without_replaying_events(backend,monkeypatch): def test_display_api_validates_payload_and_defaults_to_primary(backend): - client = TestClient(backend.app) + client = client_for(backend) assert client.post('/api/displays', json=[{}]).status_code == 422 assert client.post('/api/displays', json=[]).status_code == 400 displays = [{'index': index, 'id': index + 10, 'label': f'Screen {index}', 'primary': index == 1, @@ -329,3 +335,159 @@ def test_display_api_validates_payload_and_defaults_to_primary(backend): # Removing the selected display safely falls back to the surviving primary. assert client.post('/api/displays', json=[{**displays[1], 'index': 0}]).status_code == 200 assert backend.state.avatar_geometry['screen'] == 0 + + +def test_every_route_needs_the_install_token_and_a_loopback_host(backend): + token = backend.api_token() + assert client_for(backend).get('/api/discord/inbox').status_code == 200 + anonymous = TestClient(backend.app, base_url='http://127.0.0.1:8765') + assert anonymous.get('/api/discord/inbox').status_code == 401 # e.g. a web page in a browser tab + assert anonymous.get('/api/discord/inbox', headers={'Authorization': 'Bearer wrong'}).status_code == 401 + rebound = TestClient(backend.app, base_url='http://attacker.example:8765', headers={'Authorization': 'Bearer ' + token}) + assert rebound.get('/api/discord/inbox').status_code == 403 # DNS rebinding keeps the attacker's Host + folder = backend.config.root / 'persistent_memories' + key = backend.api_secrets()[1] + assert (folder / 'api_token').read_text() == token and (folder / 'confirm_key').read_text() == key and len(token) >= 32 and key != token + if os.name != 'nt': assert all((folder / name).stat().st_mode & 0o777 == 0o600 for name in ('api_token', 'confirm_key')) + + +def test_websockets_need_the_token_and_an_app_origin(backend): + from starlette.websockets import WebSocketDisconnect + url = 'ws://127.0.0.1:8765/ws/events' + with client_for(backend).websocket_connect(url, headers={'Origin': 'file://'}) as socket: + assert socket.receive_json()['type'] == 'state.snapshot' + with pytest.raises(WebSocketDisconnect): + with TestClient(backend.app).websocket_connect(url): pass # no token + with pytest.raises(WebSocketDisconnect): + with client_for(backend).websocket_connect(url, headers={'Origin': 'https://attacker.example'}): pass + + +def test_api_docs_are_off_outside_development(backend): + client = client_for(backend) + assert client.get('/docs').status_code == 404 and client.get('/openapi.json').status_code == 404 + + +def test_security_sensitive_settings_need_a_signature_only_electron_main_can_make(backend, monkeypatch): + from process.app_core.configuration.settings_store import SettingsStore + from process.app_core.desktop.api_guard import signature + path = backend.config.root / 'character_config.yaml' + path.write_text('runtime:\n provider: lm_studio\n') + store = SettingsStore(path) + monkeypatch.setattr(backend, 'settings_store', lambda: store) + client = client_for(backend) + body = {'revision': store.snapshot()['revision'], 'changes': {'sovits_ping_config.executable': '/tmp/not-sovits'}} + asked = client.put('/api/settings', json=body) + assert asked.status_code == 428 and path.read_text() == 'runtime:\n provider: lm_studio\n' + challenge = asked.json()['detail']['confirm'] + assert client.put('/api/settings', json=body, headers={'X-Riko-Confirmation': 'forged'}).status_code == 428 + # The API token is on the wire with every request, so it must not be able to sign confirmations. + assert client.put('/api/settings', json=body, headers={'X-Riko-Confirmation': signature(backend.api_token(), challenge)}).status_code == 428 + signed = client.put('/api/settings', json=body, headers={'X-Riko-Confirmation': signature(backend.api_secrets()[1], challenge)}) + assert signed.status_code == 200 and signed.json()['saved'] and 'not-sovits' in path.read_text() + path.write_text('runtime:\n provider: lm_studio\n') # changed back; the old approval must not apply again + replayed = client.put('/api/settings', json={**body, 'revision': store.snapshot()['revision']}, headers={'X-Riko-Confirmation': signature(backend.api_secrets()[1], challenge)}) + assert replayed.status_code == 428 and 'not-sovits' not in path.read_text() + + +def test_turning_tool_approval_off_needs_confirmation(backend, monkeypatch, tmp_path): + from process.app_core.desktop.api_guard import signature + from process.app_core.tools.registry import ToolRegistry, RegisteredTool + from process.app_core.tools.approval import ToolApprovals + registry = ToolRegistry() + registry.tools['example'] = RegisteredTool('example', 'Test tool', {}, lambda args: 'ok') + registry.approvals = ToolApprovals(tmp_path / 'approvals.json', True) + monkeypatch.setattr(backend, 'chat', SimpleNamespace(tool_registry=registry)) + client = client_for(backend) + try: + assert client.put('/api/tools/approvals', json={'policy': {'example': True}}).status_code == 200 # stricter: no prompt + asked = client.put('/api/tools/approvals', json={'policy': {'example': False}}) + assert asked.status_code == 428 and registry.approvals.snapshot()['policy']['example'] is True + proof = signature(backend.api_secrets()[1], asked.json()['detail']['confirm']) + assert client.put('/api/tools/approvals', json={'policy': {'example': False}}, headers={'X-Riko-Confirmation': proof}).status_code == 200 + assert registry.approvals.snapshot()['policy']['example'] is False + finally: registry.close() + + +def test_discord_client_receives_only_its_own_turns_not_local_activity(backend, monkeypatch): + import uuid + from process.app_core.events.bus import EventBus + bus = EventBus() + monkeypatch.setattr(backend, 'event_bus', bus) + monkeypatch.setattr(backend, 'resource_events', None) + instance = str(uuid.uuid4()) + with client_for(backend).websocket_connect(f'ws://127.0.0.1:8765/ws/discord/client?instance={instance}') as socket: + assert socket.receive_json() == {'type': 'state.snapshot', 'payload': {}} # no local desktop state + assert set(socket.receive_json()['payload']) <= {'discord', 'approvals'} + bus.publish('voice.transcript', text='private words spoken at the desk') + bus.publish('chat.delta', turn_id='local', text='a local reply', source='message') + bus.publish('model.started', turn_id='remote', source='discord') + bus.publish('chat.delta', turn_id='remote', text='hello Discord', source='discord') + bus.publish('initiative.presented', message='marker') + received = [socket.receive_json() for _ in range(2)] + assert [(event['type'], event['payload'].get('text') or event['payload'].get('message')) for event in received] == [ + ('chat.delta', 'hello Discord'), ('initiative.presented', 'marker')] + + +def test_confirmations_are_single_use_bound_to_the_change_and_expire(): + from process.app_core.desktop.api_guard import Confirmations, signature + confirmations, key = Confirmations(), 'k' * 43 + challenge = confirmations.challenge('settings', {'runtime.native_library': 'a.dll'}) + proof = signature(key, challenge) + assert not confirmations.consume(key, 'settings', {'runtime.native_library': 'b.dll'}, proof) # a different change + assert not confirmations.consume(key, 'discord_access', {'runtime.native_library': 'a.dll'}, proof) + assert not confirmations.consume('other' * 9, 'settings', {'runtime.native_library': 'a.dll'}, proof) + assert confirmations.consume(key, 'settings', {'runtime.native_library': 'a.dll'}, proof) + assert not confirmations.consume(key, 'settings', {'runtime.native_library': 'a.dll'}, proof) # replay + expired = Confirmations(ttl=-1) + challenge = expired.challenge('settings', {'tools.mcp_config': 'x'}) + assert not expired.consume(key, 'settings', {'tools.mcp_config': 'x'}, signature(key, challenge)) + + +@pytest.mark.parametrize('key', ['runtime.native_library', 'runtime.provider', 'runtime.base_url', 'runtime.model_path', + 'runtime.hf_repo_id', 'tools.mcp_config', 'emotion.enabled', 'emotion.model_id', 'memory.system1_enabled', + 'memory.system1_model_id', 'memory.store_file', 'sovits_ping_config.url', 'sovits_ping_config.ref_audio_path', + 'sovits_ping_config.executable', 'sovits_ping_config.arguments', 'voice.asr_model']) +def test_settings_that_run_code_or_send_data_elsewhere_are_security_sensitive(backend, key): + assert backend.security_sensitive(key) + + +@pytest.mark.parametrize('key', ['runtime.pause_background_on_live', 'runtime.temperature', 'avatar.scale', 'speech.max_words', 'voice.vad_threshold']) +def test_everyday_settings_save_without_confirmation(backend, key): + assert not backend.security_sensitive(key) + + +def test_sections_directories_and_locations_cannot_slip_past_confirmation(backend, monkeypatch): + sensitive = backend.security_sensitive + assert sensitive('tools', {'mcp_config': '/tmp/evil/mcp.json', 'require_approval': False}) # an empty YAML mapping is one editable value + assert sensitive('sovits_ping_config', {'nested': {'auto_start': True}}) and not sensitive('animation', {'walk_speed': 2}) + assert sensitive('desktop.effects_directory', 'effects') and sensitive('initiative.rules', [{'id': 'x', 'webhook': 'https://example.com'}]) + assert sensitive('presets.default.name', 'C:\\Windows') and sensitive('voice.wake_word', '~/words') + assert not sensitive('avatar.model', '/Users/me/Avatar.vrm') and not sensitive('presets.default.name', 'Riko') + from process.app_core.configuration.settings_store import SettingsStore + path = backend.config.root / 'character_config.yaml' + path.write_text('runtime:\n provider: lm_studio\ntools: {}\n') + store = SettingsStore(path) + monkeypatch.setattr(backend, 'settings_store', lambda: store) + asked = client_for(backend).put('/api/settings', json={'revision': store.snapshot()['revision'], 'changes': {'tools': {'mcp_config': '/tmp/evil/mcp.json'}}}) + assert asked.status_code == 428 and 'evil' not in path.read_text() + + +def test_secrets_are_fresh_each_start_and_never_inherited_by_children(tmp_path, monkeypatch): + from process.app_core.desktop.api_guard import issue_secrets, client_token + first, second = issue_secrets(tmp_path), issue_secrets(tmp_path) + assert first != second and (tmp_path / 'persistent_memories' / 'api_token').read_text() == second[0] + monkeypatch.setenv('RIKO_API_TOKEN', 'a' * 43) + monkeypatch.setenv('RIKO_CONFIRM_KEY', 'b' * 43) + assert issue_secrets(tmp_path / 'packaged') == ('a' * 43, 'b' * 43) # Electron's values, no files + assert 'RIKO_API_TOKEN' not in os.environ and 'RIKO_CONFIRM_KEY' not in os.environ + assert not (tmp_path / 'packaged' / 'persistent_memories').exists() + monkeypatch.setenv('RIKO_API_TOKEN', 'short') + monkeypatch.setenv('RIKO_CONFIRM_KEY', 'b' * 43) + token, _ = issue_secrets(tmp_path) + assert token != 'short' and 'RIKO_CONFIRM_KEY' not in os.environ + # A client such as the Discord worker finds the token beside the backend's config. + monkeypatch.setenv('RIKO_DATA_DIR', str(tmp_path)) + monkeypatch.delenv('RIKO_CONFIG', raising=False) + assert client_token() == token + monkeypatch.setenv('RIKO_API_TOKEN', 'c' * 43) + assert client_token() == 'c' * 43 diff --git a/tests/test_discord.py b/tests/test_discord.py index 7a818d4..ac8e2f6 100644 --- a/tests/test_discord.py +++ b/tests/test_discord.py @@ -34,11 +34,15 @@ def test_discord_policy_fails_closed_and_separates_admins(tmp_path): assert not any('key' in key or 'path' in key or 'command' in key for key in BASIC_SETTINGS) -@pytest.mark.parametrize('url', ['https://example.com', 'http://localhost/secret', 'http://user:pass@localhost', 'http://localhost?key=secret']) +@pytest.mark.parametrize('url', ['https://example.com', 'http://localhost/secret', 'http://user:pass@localhost', 'http://localhost?key=secret', 'http://[::1]:8765']) def test_discord_backend_url_cannot_send_runtime_secrets_remotely(tmp_path, url): with pytest.raises(ValueError): BotSettings.from_env(tmp_path, {'Discord_backend_url': url}) +def test_existing_localhost_backend_urls_keep_working_over_ipv4_loopback(tmp_path): + assert BotSettings.from_env(tmp_path, {'Discord_backend_url': 'http://localhost:8765/'}).backend_url == 'http://127.0.0.1:8765' + + def test_discord_preferences_roundtrip_and_bad_data_preserved(tmp_path): path = tmp_path / 'discord.json' prefs = Preferences(path) diff --git a/tests/test_discord_launcher.py b/tests/test_discord_launcher.py index 9e0c719..b8e665b 100644 --- a/tests/test_discord_launcher.py +++ b/tests/test_discord_launcher.py @@ -18,13 +18,19 @@ def wait(self, timeout=None): return self.code process = Process() monkeypatch.setattr(launcher.subprocess, 'Popen', lambda command, **kwargs: calls.append((command, kwargs)) or process) service = launcher.DiscordLauncher(tmp_path) + service.token = lambda: 'x' * 43 # desktop_server hands the launcher this backend start's token assert not service.status()['running'] assert calls == [] assert service.start()['running'] assert service.start()['running'] assert len(calls) == 1 assert calls[0][0] == [launcher.sys.executable, str(tmp_path / 'Code' / 'discord_bot.py')] - assert 'fake-token' not in str(calls) + assert 'fake-token' not in str(calls[0][0]) # credentials never on the command line + # The child inherits the environment as before, plus only the backend API token, data root and exit rule. + env = calls[0][1]['env'] + assert set(env) - set(launcher.os.environ) == {'RIKO_API_TOKEN', 'RIKO_DATA_DIR', 'RIKO_EXIT_WITH_BACKEND'} + assert calls[0][1]['stdin'] == launcher.subprocess.PIPE # closes when this backend exits + assert env['RIKO_API_TOKEN'] == 'x' * 43 and env['RIKO_DATA_DIR'] == str(tmp_path) assert not calls[0][1].get('shell') service.stop() assert process.code == 0 @@ -49,3 +55,11 @@ def test_launcher_reports_exit_without_exposing_child_output(tmp_path): service._watch(process) assert not service.status()['running'] assert 'exited' in service.status()['error'] + + +def test_worker_started_by_the_backend_exits_when_the_backend_goes_away(): + import subprocess, sys + code = "import sys, types; sys.path.insert(0, 'Code'); import discord_bot; discord_bot.exit_with_backend(types.SimpleNamespace())" + child = subprocess.Popen([sys.executable, '-c', code], stdin=subprocess.PIPE) + child.stdin.close() # what the OS does when the backend process ends + assert child.wait(timeout=20) == 0 From 0de0522fd8fb5213c2a80c82522bd99a3df5294c Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 14:39:53 +0530 Subject: [PATCH 07/37] Add a llama_server provider for a llama-server the user runs Port the llama.cpp branch's approach (talk to an external llama-server via its base URL and /tokenize) onto main's shared LlamaContextProvider, so any llama.cpp build (CUDA, ROCm/HIP, Metal, Vulkan, CPU) can serve Riko while it keeps the reserved live slot, exact template/tokenizer counts, Responses streaming, tools and cancellation. - LlamaServerProvider waits on /health while the model loads, then requires parallel_slots slots each holding the largest live/initiative/reflection budget, and prints the matching llama-server command if not. - One http.client connection per request; cancelling shuts the socket down, so a request stopped during prefill returns at once and llama-server stops it too. Connects time out after 5 s, and a lost server is checked again on the next request. - runtime.base_url (default http://127.0.0.1:8080) and api_key are validated when the config loads, so Settings rejects a bad address before saving. - Shared slot validation moves into validate_slots; native-only checks stay with InProcessLlamaProvider. The emotion probe remains native-only. - Settings shows the server address, API key and slot fields for this provider with the launch command; README and stale messages point to it. Verified against stock llama-server b11408 with a local Qwen3 4B GGUF on Apple Silicon: warmup, exact counts, streaming timings, tool calls, and cancellation mid-prefill and mid-stream (0.04 s). Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- Code/process/app_core/configuration/config.py | 3 +- .../app_core/configuration/settings_store.py | 9 +- .../app_core/inference/llama_context.py | 20 +- .../app_core/inference/llama_native.py | 8 +- .../app_core/inference/llama_runtime.py | 39 +++- .../app_core/inference/llama_server.py | 196 +++++++++++++++++ Code/process/app_core/inference/providers.py | 5 +- README.md | 8 +- electron/src/settings_model.mjs | 10 + electron/src/settings_model.test.mjs | 8 +- electron/src/settings_page.jsx | 7 +- tests/test_llama_server.py | 198 ++++++++++++++++++ 13 files changed, 486 insertions(+), 28 deletions(-) create mode 100644 Code/process/app_core/inference/llama_server.py create mode 100644 tests/test_llama_server.py diff --git a/CLAUDE.md b/CLAUDE.md index 1dc7128..8e6271f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. ### Inference - `create_provider` (`app_core/inference/providers.py`) picks the provider: - `llama_cpp` → `InProcessLlamaProvider` (`llama_native.py`). It calls the C ABI in `tools/llama_cpp/riko-native.cpp` via ctypes: five `riko_*` exports, serving only `/v1/responses`, `/apply-template`, `/tokenize`, `/props` and `/slots`, with argv from `native_arguments` in `llama_context.py`. There is no HTTP listener, llama-server or llama-cpp-python, and no fallback: `runtime.native_library` is required. + - `llama_server` → `LlamaServerProvider` (`llama_server.py`): the same `LlamaContextProvider` protocol (slot 0 for live, exact `/apply-template` + `/tokenize` counts, `/v1/responses` streaming and tools) against a llama-server the user runs at `runtime.base_url` (default `http://127.0.0.1:8080`; validated by `server_address` in `llama_runtime.py`). It uses `http.client`, one connection per request, and cancels by shutting the socket down (closing alone does not wake a read blocked during prefill). Startup waits on `/health` (503 while loading), then requires exactly `parallel_slots` slots each holding the largest of the live, initiative and reflection budgets, and prints the matching `llama-server` command otherwise; a connection failure resets it so the next request checks again. The server owns the model and GPU settings, and there is no emotion probe. - `openai`, `lm_studio`, `openai_compatible`, `ollama`, `local_http` → `OpenAIProvider`: Responses API with a Chat Completions fallback, byte-based token estimates, no slots, no provider-level cancel. - `SlotScheduler` (`llama_context.py`) reserves slot 0 for the `live` lane. `initiative` and `reflection` share slots 1..N-1 (`runtime.parallel_slots`, 2–4), and live turns preempt them when `runtime.pause_background_on_live` is set. The emotion probe captures only on slot 0. - `--ctx-size` comes from `kv_budget.pool_capacity`. Per-role budgets are enforced only by Python-side packing (`context_budget.pack_context`, with exact native token counts) and by `max_output_tokens`. @@ -168,7 +169,7 @@ The data root is the directory holding `character_config.yaml`. ## Platform status -Releases ship CUDA and Vulkan bundles for Windows and Linux x64 only; nothing builds, packages, detects or tests Metal or ROCm. The Python inference layer is backend-agnostic: the backend is whatever llama.cpp build `runtime.native_library` loads. The patched bridge, including the emotion probe, has been built and run on Apple Silicon Metal at the current pin. +Releases ship CUDA and Vulkan bundles for Windows and Linux x64 only; nothing builds, packages, detects or tests Metal or ROCm. The Python inference layer is backend-agnostic: the backend is whatever llama.cpp build `runtime.native_library` loads, or whatever llama-server `runtime.provider: llama_server` talks to (any backend, without the emotion probe; checked against stock llama-server b11408 on Apple Silicon). The patched bridge, including the emotion probe, has been built and run on Apple Silicon Metal at the current pin. Backend and device assumptions live here: - **Native build and packaging:** diff --git a/Code/process/app_core/configuration/config.py b/Code/process/app_core/configuration/config.py index f2091b4..9dd1eb6 100644 --- a/Code/process/app_core/configuration/config.py +++ b/Code/process/app_core/configuration/config.py @@ -165,7 +165,8 @@ def load_config(path: str | Path | None = None) -> AppConfig: runtime = RuntimeConfig( provider=runtime_raw.get("provider", "openai"), model=runtime_raw.get("model", raw.get("model", "")), - base_url=runtime_raw.get("base_url", raw.get("base_url", "http://localhost:1234/v1")), + base_url=runtime_raw.get("base_url", raw.get("base_url", "http://127.0.0.1:8080" + if str(runtime_raw.get("provider", "")).lower().replace("-", "_") == "llama_server" else "http://localhost:1234/v1")), api_key=runtime_raw.get("api_key", raw.get("api_key", os.getenv("RIKO_API_KEY", "local"))), model_path=_path(root, runtime_raw.get("model_path")), native_library=_path(root, library), diff --git a/Code/process/app_core/configuration/settings_store.py b/Code/process/app_core/configuration/settings_store.py index b764cc9..e2be747 100644 --- a/Code/process/app_core/configuration/settings_store.py +++ b/Code/process/app_core/configuration/settings_store.py @@ -20,7 +20,7 @@ 'model','base_url','api_key','tokenizer_model'} ENUMS = { 'logging.level': ['DEBUG','INFO','WARNING','ERROR'], - 'runtime.provider': ['llama_cpp', 'lm_studio', 'openai', 'openai_compatible', 'ollama', 'local_http'], + 'runtime.provider': ['llama_cpp', 'llama_server', 'lm_studio', 'openai', 'openai_compatible', 'ollama', 'local_http'], 'runtime.api_mode': ['auto', 'responses', 'chat_completions'], 'runtime.type_k': ['f16', 'f32', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl'], 'runtime.type_v': ['f16', 'f32', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl'], @@ -70,7 +70,9 @@ 'sovits_ping_config.auto_start': 'Packaged app only: explicitly launch your GPT-SoVITS API executable on startup. The app stops only its owned direct process on quit. Save and restart the app, not just Python.', 'sovits_ping_config.executable': 'Absolute path to a GPT-SoVITS API-server executable, not a GUI launcher. Its dependencies and model weights must already be installed. Save and restart the packaged app.', 'sovits_ping_config.arguments': 'JSON list of command-line arguments passed directly without a shell. Configure its API port to match the endpoint URL. Restart the packaged app.', - 'runtime.startup_timeout_seconds': 'Deadline for optional component warmup (ASR, TTS and auxiliary models), not a timeout for synchronous native model loading. Native request waits use Request timeout. Restart Python.', + 'runtime.startup_timeout_seconds': 'Deadline for optional component warmup (ASR, TTS and auxiliary models), and for an external llama-server to finish loading its model; not a timeout for synchronous native model loading. Native request waits use Request timeout. Restart Python.', + 'runtime.provider': 'llama_cpp runs llama.cpp inside Python with a riko-native library. llama_server connects to a llama-server you run, built for any backend (CUDA, ROCm, Metal, Vulkan or CPU), and keeps slots, exact token counts and streaming. The others use OpenAI-compatible APIs. Save and restart Python.', + 'runtime.base_url': 'Server address. For llama_server, the llama-server address such as http://127.0.0.1:8080; its --parallel must equal Parallel inference slots and each slot needs at least the live, initiative and reflection context. Conversations are sent to this address. Save and restart Python.', 'emotion.probe.enabled': 'Collect hidden-state features and Julia targets with a compatible in-process DLL. Julia stays live for user transcription. A validated probe can take over agent expressions; no extra main model is loaded. Restart Python.', 'emotion.probe.use_for_expression': 'Automatically use the probe for agent expressions only after held-out validation passes. Julia remains for user input and low-confidence or unavailable-feature fallback. Disable to continue gathering comparisons. Restart Python.', 'emotion.probe.auto_train': 'Train automatically only after continuous idle time and sufficient new samples. Inference, microphone activity and playback take priority. Manual Train now skips the idle delay, not foreground protection. Restart Python.', @@ -258,7 +260,8 @@ def snapshot(self): values = self._values(raw) return {'revision': revision(text), 'values': values, 'fields': [field(k, v) for k, v in values.items() if k not in OBSOLETE and not k.startswith('presets.default.model_params.') - and not (values.get('runtime.provider') == 'llama_cpp' and k in {'runtime.model','runtime.base_url','runtime.api_key','runtime.api_mode','runtime.reuse_response_ids','runtime.tokenizer_model'})], + and not (values.get('runtime.provider') == 'llama_cpp' and k in {'runtime.model','runtime.api_mode','runtime.reuse_response_ids','runtime.tokenizer_model'}) + and not (values.get('runtime.provider') == 'llama_server' and k in {'runtime.model','runtime.api_mode','runtime.reuse_response_ids','runtime.tokenizer_model'})], 'path': str(self.path), 'restart_required': True} def _values(self, raw): diff --git a/Code/process/app_core/inference/llama_context.py b/Code/process/app_core/inference/llama_context.py index 013b6f8..17d494d 100644 --- a/Code/process/app_core/inference/llama_context.py +++ b/Code/process/app_core/inference/llama_context.py @@ -1,4 +1,5 @@ -"""Native model request protocol, context budgeting and priority-ordered slots.""" +"""llama.cpp request protocol, context budgeting and priority-ordered slots, shared by the +in-process native library and an external llama-server.""" from contextlib import contextmanager import logging import queue @@ -8,7 +9,7 @@ import uuid import math -from .llama_runtime import validate_runtime +from .llama_runtime import validate_slots logger = logging.getLogger(__name__) @@ -119,10 +120,11 @@ def count_tokens(self, messages): return self.owner.count_tokens(messages) class LlamaContextProvider(InferenceLane): supports_latent_probe = True + transport = 'Native llama.cpp' # names the transport in errors + missing_route_hint = ' Rebuild the compatible riko-native library with Responses support.' def __init__(self, config): - validate_runtime(config) - context_capacity(config) - if not config.n_ctx: raise ValueError('Native llama.cpp requires explicit per-slot runtime.n_ctx > 0') + validate_slots(config) + if not config.n_ctx: raise ValueError('llama.cpp requires explicit per-slot runtime.n_ctx > 0') super().__init__(self, 'live') self.config = config self.scheduler = SlotScheduler(config.parallel_slots, pause_background=config.pause_background_on_live) @@ -258,8 +260,8 @@ def chunks(): def _inference_client(self): raise NotImplementedError('Native transport must provide a request client') - @staticmethod - def _check_response(response): + @classmethod + def _check_response(cls, response): if response.is_success: return response.read() try: @@ -267,8 +269,8 @@ def _check_response(response): error = body.get('error', body) detail = error.get('message', str(error)) if isinstance(error, dict) else str(error) except (ValueError, AttributeError): detail = response.text - hint = ' Rebuild the compatible riko-native library with Responses support.' if response.status_code in {404, 405, 501} else '' - raise RuntimeError(f'Native llama.cpp operation {response.status_code}: {str(detail).strip()[:2000]}{hint}') + hint = cls.missing_route_hint if response.status_code in {404, 405, 501} else '' + raise RuntimeError(f'{cls.transport} operation {response.status_code}: {str(detail).strip()[:2000]}{hint}') def stream(self, messages, *, tools=None, **options): output = queue.Queue(maxsize=128) diff --git a/Code/process/app_core/inference/llama_native.py b/Code/process/app_core/inference/llama_native.py index 99b787f..a750e31 100644 --- a/Code/process/app_core/inference/llama_native.py +++ b/Code/process/app_core/inference/llama_native.py @@ -14,8 +14,8 @@ import threading import time -from .llama_context import LlamaContextProvider, native_arguments -from .llama_runtime import resolve_model +from .llama_context import LlamaContextProvider, context_capacity, native_arguments +from .llama_runtime import resolve_model, validate_runtime logger = logging.getLogger(__name__) OUTPUT = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p) @@ -229,6 +229,8 @@ def get(self, path, **kwargs): return self.post(path) class InProcessLlamaProvider(LlamaContextProvider): def __init__(self, config): + validate_runtime(config) + context_capacity(config) super().__init__(config) self.native = None self.probe_interval = 32 @@ -238,7 +240,7 @@ def _start(self): if self.closed: raise RuntimeError('Native provider closed') if self.native: return if not self.config.native_library: - raise RuntimeError('Set runtime.native_library to a compatible riko-native library. The HTTP llama-server backend has been removed.') + raise RuntimeError('Set runtime.native_library to a compatible riko-native library, or set runtime.provider: llama_server to use a llama-server you run.') if not Path(self.config.native_library).is_file(): raise RuntimeError(f'runtime.native_library does not exist: {self.config.native_library}. Build or select a compatible riko-native library before loading the model.') model = resolve_model(self.config) diff --git a/Code/process/app_core/inference/llama_runtime.py b/Code/process/app_core/inference/llama_runtime.py index e1c2add..3a791f2 100644 --- a/Code/process/app_core/inference/llama_runtime.py +++ b/Code/process/app_core/inference/llama_runtime.py @@ -1,7 +1,8 @@ -"""Validated native settings and reproducible GGUF resolution (no model imports).""" +"""Validated llama.cpp settings and reproducible GGUF resolution (no model imports).""" import math from pathlib import Path, PurePosixPath import re +from urllib.parse import urlsplit KV_TYPES = {'f32', 'f16', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl'} EXTRA_SETTINGS = ('hf_repo_id', 'hf_filename', 'hf_revision', 'hf_local_files_only', @@ -14,24 +15,52 @@ def configure_runtime(config, raw): for key in EXTRA_SETTINGS: if key in raw: setattr(config, key, raw[key]) - if config.provider.lower().replace('-', '_') == 'llama_cpp': + provider = config.provider.lower().replace('-', '_') + if provider == 'llama_cpp': for key in ('n_ctx', 'n_batch', 'n_gpu_layers'): if key in raw and type(raw[key]) is not int: raise ValueError(f'runtime.{key} must be an integer') if 'n_ubatch' not in raw: config.n_ubatch = min(512, config.n_batch) validate_runtime(config) + elif provider == 'llama_server': + if 'n_ctx' in raw and type(raw['n_ctx']) is not int: raise ValueError('runtime.n_ctx must be an integer') + if config.base_url is None: config.base_url = 'http://127.0.0.1:8080' + server_address(config.base_url) + if config.api_key is not None and not isinstance(config.api_key, str): + raise ValueError('runtime.api_key must be text; put it in quotes in character_config.yaml') + validate_slots(config) -def validate_runtime(config): +def server_address(url): + """The llama-server root for runtime.base_url; it serves /tokenize, /slots and /health beside /v1.""" + problem = ValueError('runtime.base_url must be the llama-server address, such as http://127.0.0.1:8080') + if not isinstance(url, str): raise problem + root = url.strip().rstrip('/') + root = root[:-3] if root.endswith('/v1') else root + parts = urlsplit(root) + try: parts.port + except ValueError: raise problem from None + if parts.scheme not in {'http', 'https'} or not parts.hostname or parts.username or parts.password or parts.path or parts.query or parts.fragment: + raise problem + return root + + +def validate_slots(config): + """Settings shared by every llama.cpp transport: Riko schedules the slots and packs each request.""" if type(config.parallel_slots) is not int or not 2 <= config.parallel_slots <= 4: raise ValueError('runtime.parallel_slots must be an integer from 2 to 4') if type(config.warmup) is not bool: raise ValueError('runtime.warmup must be boolean') if type(config.pause_background_on_live) is not bool: raise ValueError('runtime.pause_background_on_live must be boolean') + if type(config.startup_timeout_seconds) not in (int, float) or not math.isfinite(config.startup_timeout_seconds) or config.startup_timeout_seconds <= 0: + raise ValueError('runtime.startup_timeout_seconds must be positive and finite') + if type(config.n_ctx) is not int or config.n_ctx < 0: raise ValueError('runtime.n_ctx must be an integer >= 0') + + +def validate_runtime(config): + validate_slots(config) if type(config.kv_unified) is not bool: raise ValueError('runtime.kv_unified must be boolean') if type(config.kv_pool_auto) is not bool: raise ValueError('runtime.kv_pool_auto must be boolean') if config.kv_pool_tokens is not None and (type(config.kv_pool_tokens) is not int or not 1 <= config.kv_pool_tokens <= 4194304): raise ValueError('runtime.kv_pool_tokens must be null or an integer from 1 to 4194304') - if type(config.startup_timeout_seconds) not in (int, float) or not math.isfinite(config.startup_timeout_seconds) or config.startup_timeout_seconds <= 0: - raise ValueError('runtime.startup_timeout_seconds must be positive and finite') for key in ('n_ctx', 'n_batch', 'n_ubatch', 'main_gpu', 'cache_size_mb'): value = getattr(config, key) minimum = 1 if key in {'n_batch', 'n_ubatch'} else 0 diff --git a/Code/process/app_core/inference/llama_server.py b/Code/process/app_core/inference/llama_server.py new file mode 100644 index 0000000..d3f27c1 --- /dev/null +++ b/Code/process/app_core/inference/llama_server.py @@ -0,0 +1,196 @@ +"""External llama-server transport: Riko talks to a llama-server that the user runs. + +Any llama.cpp build works (CUDA, ROCm/HIP, Metal, Vulkan or CPU), on this machine or +another one. Riko keeps its scheduling: slot 0 is reserved for the live lane, every +request is packed with the server's own template and tokenizer, and cancelling a request +shuts its connection down. The server owns the model, its GPU settings and the context it +allocated, so the runtime.* model and compute settings do not apply. Stock servers expose +no hidden states, so the emotion probe needs the native library (runtime.provider: llama_cpp). +""" +import http.client +from json import dumps, loads +import logging +import socket +import threading +import time +from urllib.parse import urlsplit + +from .llama_context import LlamaContextProvider +from .llama_runtime import server_address + +logger = logging.getLogger(__name__) +CONNECT_TIMEOUT = 5.0 + + +class Unreachable(ConnectionError): pass + + +class ServerResponse: + """The parts of an httpx-style response that LlamaContextProvider uses.""" + def __init__(self, client, connection, response): + self.client, self.connection, self.response = client, connection, response + self.status_code, self.content, self.consumed = response.status, b'', False + + @property + def is_success(self): return 200 <= self.status_code < 300 + + @property + def text(self): return self.content.decode('utf-8', errors='replace') + + def read(self): + if not self.consumed: self.content, self.consumed = self.response.read(), True + return self.content + + def json(self): return loads(self.read()) + + def raise_for_status(self): + if not self.is_success: raise RuntimeError(f'llama-server operation {self.status_code}: {self.read().decode("utf-8", errors="replace")[:2000]}') + + def iter_lines(self): + while True: + line = self.response.readline(1 << 20) + if not line: return + yield line.rstrip(b'\r\n').decode('utf-8') + + def close(self): self.client._release(self.connection) + def __enter__(self): return self + def __exit__(self, *args): self.close() + + +class ServerClient: + """One connection per request, so threads never share one and any of them can be cancelled. + + close() shuts the open sockets down: that wakes a read blocked while llama-server prefills + (closing alone does not, and on Linux does not even send FIN) and stops the server's work at once. + """ + def __init__(self, root, api_key, timeout): + parts = urlsplit(root) + self.kind = http.client.HTTPSConnection if parts.scheme == 'https' else http.client.HTTPConnection + self.host, self.port, self.timeout = parts.hostname, parts.port, timeout + self.headers = {'Authorization': 'Bearer ' + api_key} if api_key else {} + self.connections, self.lock, self.closed = {}, threading.Lock(), False # connection -> its socket + + def __enter__(self): return self + def __exit__(self, *args): self.close() + + def _open(self, timeout): + if self.closed: raise RuntimeError('llama-server request cancelled') + connection = self.kind(self.host, self.port, timeout=min(CONNECT_TIMEOUT, timeout)) + try: connection.connect() + except OSError as exc: + connection.close() + raise Unreachable(str(exc) or type(exc).__name__) from None + connection.sock.settimeout(timeout) # then it bounds inactivity, not the whole reply + with self.lock: + if not self.closed: + # Kept here because http.client hands the socket to the response (and forgets it) on Connection: close. + self.connections[connection] = connection.sock + return connection + connection.close() + raise RuntimeError('llama-server request cancelled') + + def _release(self, connection): + with self.lock: self.connections.pop(connection, None) + connection.close() + + def stream(self, method, path, json=None, timeout=None): + connection = self._open(self.timeout if timeout is None else timeout) + try: + body = None if json is None else dumps(json).encode() + headers = {**self.headers, **({'Content-Type': 'application/json'} if body is not None else {})} + connection.request(method, path, body=body, headers=headers) + return ServerResponse(self, connection, connection.getresponse()) + except BaseException: + self._release(connection) + raise + + def post(self, path, json=None, timeout=None): + with self.stream('POST', path, json, timeout) as response: + response.read() + return response + + def get(self, path, timeout=None): + with self.stream('GET', path, None, timeout) as response: + response.read() + return response + + def close(self): + with self.lock: + self.closed = True + sockets = list(self.connections.values()) + for sock in sockets: # each request's own thread closes its connection + try: sock.shutdown(socket.SHUT_RDWR) + except OSError: pass + + +class LlamaServerProvider(LlamaContextProvider): + supports_latent_probe = False + transport = 'llama-server' + missing_route_hint = ' Update llama-server to a recent llama.cpp build; Riko needs /v1/responses, /apply-template, /tokenize and /slots.' + + def __init__(self, config): + super().__init__(config) + self.root = server_address(config.base_url) + + def _client(self): return ServerClient(self.root, self.config.api_key, self.config.request_timeout_seconds) + + def _required_context(self): + return max(self.config.n_ctx, getattr(self.config, 'initiative_n_ctx', 4096), getattr(self.config, 'reflection_n_ctx', 4096)) + + def launch_hint(self): + slots, port = self.config.parallel_slots, urlsplit(self.root).port or 8080 + return (f'Start it with: llama-server -m --port {port} --parallel {slots} ' + f'--ctx-size {slots * self._required_context()} --jinja (or change runtime.parallel_slots and runtime.n_ctx to match the server).') + + def _unreachable(self, error): + # Check /health and /slots again on the next request: the server may come back with other settings. + self.client = None + return RuntimeError(f'No llama-server is answering at {self.root} ({error}). {self.launch_hint()}') + + def _wait_until_ready(self, client): + deadline = time.monotonic() + self.config.startup_timeout_seconds + while True: + if self.closed: raise RuntimeError('llama-server provider closed') + try: status = client.get('/health', timeout=CONNECT_TIMEOUT).status_code + except Unreachable as exc: raise self._unreachable(exc) from None + except (OSError, http.client.HTTPException): status = None + if status == 200: return + if status in {401, 403}: raise RuntimeError(f'llama-server at {self.root} refused runtime.api_key ({status}).') + if status not in {None, 503}: raise RuntimeError(f'{self.root} answered /health with {status}; is runtime.base_url a llama-server?') + if time.monotonic() >= deadline: + raise TimeoutError(f'llama-server at {self.root} was still loading after runtime.startup_timeout_seconds') + time.sleep(.25) # 503 while the server loads its model + + def _start(self): + with self.start_lock: + if self.closed: raise RuntimeError('llama-server provider closed') + if self.client: return + client = self._client() + self._wait_until_ready(client) + try: response = client.get('/slots') + except Unreachable as exc: raise self._unreachable(exc) from None + if response.status_code in {404, 501}: + raise RuntimeError(f'llama-server at {self.root} does not expose /slots; start it without --no-slots. {self.launch_hint()}') + self._check_response(response) + slots, required = response.json(), self._required_context() + if not isinstance(slots, list) or len(slots) != self.config.parallel_slots or any( + not isinstance(slot, dict) or type(slot.get('n_ctx')) is not int or slot['n_ctx'] < required for slot in slots): + found = ', '.join(str(slot.get('n_ctx')) if isinstance(slot, dict) else '?' for slot in slots) if isinstance(slots, list) else 'none' + raise RuntimeError(f'llama-server at {self.root} has slots with [{found}] tokens of context; Riko needs ' + f'{self.config.parallel_slots} slots with at least {required} each. {self.launch_hint()}') + self.client = client + logger.info('Inference transport=llama_server url=%s slots=%s per_slot_context=%s', self.root, len(slots), min(slot['n_ctx'] for slot in slots)) + + def _inference_client(self): return self._client() + + def _generate(self, role, messages, **options): + try: return super()._generate(role, messages, **options) + except Unreachable as exc: raise self._unreachable(exc) from None + + def count_tokens(self, messages): + try: return super().count_tokens(messages) + except Unreachable as exc: raise self._unreachable(exc) from None + + def count_text_tokens(self, text): + try: return super().count_text_tokens(text) + except Unreachable as exc: raise self._unreachable(exc) from None diff --git a/Code/process/app_core/inference/providers.py b/Code/process/app_core/inference/providers.py index c6979f3..e6c4a27 100644 --- a/Code/process/app_core/inference/providers.py +++ b/Code/process/app_core/inference/providers.py @@ -221,9 +221,12 @@ def create_provider(config): if provider == "llama_cpp": from .llama_native import InProcessLlamaProvider return InProcessLlamaProvider(config) + if provider == "llama_server": + from .llama_server import LlamaServerProvider + return LlamaServerProvider(config) if provider in {"openai", "lm_studio", "openai_compatible", "ollama", "local_http"}: return OpenAIProvider(config) raise ValueError( - f"Unsupported provider '{config.provider}'. Use llama_cpp, openai, lm_studio, " + f"Unsupported provider '{config.provider}'. Use llama_cpp, llama_server, openai, lm_studio, " "openai_compatible, ollama, or local_http." ) diff --git a/README.md b/README.md index d4c26a5..794ecbe 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Review `character_config.yaml` before starting: `riko-native` library. Set `runtime.native_library` to a compatible build and select a local GGUF or exact Hugging Face file/revision. No HTTP listener, llama-server process or llama-cpp-python binding is used. See [model runtime](docs/llama-runtime.md). +- `runtime.provider: llama_server` connects to a llama-server you run yourself, built + for any llama.cpp backend (CUDA, ROCm/HIP, Metal, Vulkan or CPU), on this machine or + another. Set `runtime.base_url` (default `http://127.0.0.1:8080`) and start it with + `--parallel` equal to `runtime.parallel_slots` and enough `--ctx-size` for every slot; + Riko prints the exact command if they do not match. Riko keeps its reserved live slot, + exact token counts, streaming and cancellation. The emotion probe needs `llama_cpp`. - Remote OpenAI-compatible providers, including LM Studio, are optional alternatives. - Start GPT-SoVITS separately. `sovits_ping_config` configures its HTTP endpoint, reference audio/transcript and PCM sample rate. Python does not load its model. @@ -225,4 +231,4 @@ Settings -> Performance & logs includes logging level (DEBUG, INFO, WARNING, ERR Chat displays live llama.cpp per-request token counts and tokens/second from `timings_per_token`. Other providers use explicitly labelled estimates until final usage arrives. Counts include reasoning tokens. Julia remains live for cumulative partial user transcriptions. Settings -> Custom neural network settings manages the agent-expression probe and training data. -`runtime.native_library` is required for native llama.cpp inference. There is no external-server fallback; unset paths produce an actionable startup error. Choose a GPU-enabled build for GPU inference: requesting GPU layers alone does not prove backend support. Remote OpenAI-compatible providers remain available. +`runtime.native_library` is required for native llama.cpp inference (`llama_cpp`); unset paths produce an actionable startup error. To use a llama.cpp build Riko does not bundle, run llama-server and choose `llama_server` instead. Choose a GPU-enabled build for GPU inference: requesting GPU layers alone does not prove backend support. Remote OpenAI-compatible providers remain available. diff --git a/electron/src/settings_model.mjs b/electron/src/settings_model.mjs index 6e237ff..1adcf3f 100644 --- a/electron/src/settings_model.mjs +++ b/electron/src/settings_model.mjs @@ -32,6 +32,16 @@ export function settingsPatch(fields, values, inputs) { return {changes, errors}; } +// The llama-server command for these settings: each slot needs room for the largest of the live, +// initiative and reflection budgets (as the backend checks), on the port in the server address. +export function llamaServerCommand(inputs) { + const slots = Number(inputs['runtime.parallel_slots']) || 2; + const perSlot = Math.max(Number(inputs['runtime.n_ctx']) || 0, Number(inputs['initiative.context_window_tokens']) || 4096, Number(inputs['memory.reflection_context_window_tokens']) || 4096); + let port = '8080'; + try { port = new URL(String(inputs['runtime.base_url'] || 'http://127.0.0.1:8080')).port || '8080'; } catch {} + return `llama-server -m model.gguf --port ${port} --parallel ${slots} --ctx-size ${slots * perSlot} --jinja`; +} + export const runtimePresets = { balanced: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 512, 'runtime.n_ubatch': 512, 'runtime.flash_attn': false, 'runtime.type_k': 'f16', 'runtime.type_v': 'f16'}, compact: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 256, 'runtime.n_ubatch': 128, 'runtime.flash_attn': true, 'runtime.type_k': 'q8_0', 'runtime.type_v': 'q8_0'}, diff --git a/electron/src/settings_model.test.mjs b/electron/src/settings_model.test.mjs index dea3278..a62fb7e 100644 --- a/electron/src/settings_model.test.mjs +++ b/electron/src/settings_model.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import {test} from 'node:test'; -import {parseSetting,settingsPatch,inputValues} from './settings_model.mjs'; +import {parseSetting,settingsPatch,inputValues,llamaServerCommand} from './settings_model.mjs'; test('numeric inputs validate immediately and do not coerce blanks to zero',()=>{ const field={kind:'number',integer:true,min:2,max:4}; @@ -27,3 +27,9 @@ test('form hydration preserves zero, false and JSON arrays',()=>{ const inputs=inputValues({values,fields}); assert.equal(inputs.zero,0);assert.equal(inputs.flag,false);assert.deepEqual(JSON.parse(inputs.split),[1,1]); }); + +test('the llama-server command matches what the backend checks: largest budget per slot, configured port',()=>{ + assert.equal(llamaServerCommand({'runtime.parallel_slots':2,'runtime.n_ctx':'6144','initiative.context_window_tokens':8192,'memory.reflection_context_window_tokens':4096,'runtime.base_url':'http://127.0.0.1:8081/v1'}), + 'llama-server -m model.gguf --port 8081 --parallel 2 --ctx-size 16384 --jinja'); + assert.equal(llamaServerCommand({'runtime.parallel_slots':'3','runtime.n_ctx':16384,'runtime.base_url':'not a url'}), 'llama-server -m model.gguf --port 8080 --parallel 3 --ctx-size 49152 --jinja'); +}); diff --git a/electron/src/settings_page.jsx b/electron/src/settings_page.jsx index 5b61e5f..fe5cc07 100644 --- a/electron/src/settings_page.jsx +++ b/electron/src/settings_page.jsx @@ -1,7 +1,7 @@ import React, {useEffect, useMemo, useRef, useState} from 'react'; import {Save, FolderOpen, Check, RefreshCw, AlertCircle, Plus, Trash2} from './ui/icons.jsx'; import {request} from './api.mjs'; -import {inputValues, settingsPatch, runtimePresets} from './settings_model.mjs'; +import {inputValues, settingsPatch, runtimePresets, llamaServerCommand} from './settings_model.mjs'; import InitiativeSettings from './initiative_settings.jsx'; import DisplaySettings from './display_settings.jsx'; import VoiceInput from './voice_input.jsx'; @@ -60,7 +60,7 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) },[dirty]); const errors={...serverErrors,...parsed.errors}; if(inputs['runtime.provider']==='llama_cpp'&&modelSource==='local'&&!inputs['runtime.model_path'])errors['runtime.model_path']='Choose a local GGUF file'; - if(inputs['runtime.provider']==='llama_cpp'&&!inputs['runtime.native_library'])errors['runtime.native_library']='Choose a compatible riko-native library; the HTTP server backend is no longer supported'; + if(inputs['runtime.provider']==='llama_cpp'&&!inputs['runtime.native_library'])errors['runtime.native_library']='Choose a compatible riko-native library, or choose llama_server to use a llama-server you run'; if(inputs['runtime.provider']==='llama_cpp'&&inputs['runtime.model_path']&&!String(inputs['runtime.model_path']).toLowerCase().endsWith('.gguf'))errors['runtime.model_path']='Choose a .gguf model file'; if(inputs['runtime.provider']==='llama_cpp'&&!inputs['runtime.model_path']&&files.length&&inputs['runtime.hf_filename']&&!files.includes(inputs['runtime.hf_filename']))errors['runtime.hf_filename']='File is not in this repository/revision listing'; async function load() { @@ -137,7 +137,7 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) &&!['avatar.model','avatar.format','runtime.kv_pool_auto','runtime.kv_pool_tokens'].includes(field.path) &&(advanced||!field.advanced) &&(advanced||group!=='models'||inputs['runtime.provider']!=='llama_cpp'||(modelSource==='local'?!field.path.startsWith('runtime.hf_'):field.path!=='runtime.model_path')) - &&(advanced||group!=='models'||!(inputs['runtime.provider']==='llama_cpp'?['runtime.api_mode','runtime.reuse_response_ids','runtime.base_url','runtime.api_key','runtime.model'].includes(field.path):field.path.startsWith('runtime.')&&!['runtime.provider','runtime.api_mode','runtime.reuse_response_ids','runtime.base_url','runtime.api_key','runtime.model','runtime.temperature','runtime.max_output_tokens','runtime.n_ctx','runtime.request_timeout_seconds','runtime.warmup'].includes(field.path))))); + &&(advanced||group!=='models'||!(inputs['runtime.provider']==='llama_cpp'?['runtime.api_mode','runtime.reuse_response_ids','runtime.base_url','runtime.api_key','runtime.model'].includes(field.path):field.path.startsWith('runtime.')&&!['runtime.provider','runtime.api_mode','runtime.reuse_response_ids','runtime.base_url','runtime.api_key','runtime.model','runtime.temperature','runtime.max_output_tokens','runtime.n_ctx','runtime.request_timeout_seconds','runtime.warmup',...(inputs['runtime.provider']==='llama_server'?['runtime.parallel_slots','runtime.startup_timeout_seconds','runtime.pause_background_on_live']:[])].includes(field.path))))); const sections=[...new Set(fields.map(field=>field.section||'Settings'))].sort((a,b)=>{ const order=['Model source','Token budgets','Generation','Scheduling & startup','Compute & cache','Speech recognition model','Background models','Embedding model','Emotion model','Renderer assets','Preset fallbacks','Settings','Emotion'];return order.indexOf(a)-order.indexOf(b); }); @@ -179,6 +179,7 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) {group==='appearance'&&<>

Model setup, animation connections, physics and effects live here. Interface colors and speech popups remain in Appearance. Local model overrides apply immediately.

} {group==='tools'&&
} {group==='neural'&&} + {group==='models'&&inputs['runtime.provider']==='llama_server'&&

Your llama-server

Riko connects to a llama-server you start yourself, built for any backend: CUDA, ROCm, Metal, Vulkan or CPU. Riko still reserves a slot for live replies, counts tokens exactly and streams. The server chooses the model and GPU settings; the emotion probe needs the native library instead.

Start it with {llamaServerCommand(inputs)}, set the address below, then Save and restart Python.

} {group==='models'&&inputs['runtime.provider']==='llama_cpp'&&

How llama.cpp runs

{inputs['runtime.native_library']?'Draft: in-process native library':'Native library required — select a compatible riko-native library'} · Changes take effect after Save and a Python restart.

llama.cpp runs inside Python with one main model, reserved inference slots, streaming, cancellation, tools and prompt caching. There is no separate llama-server process or HTTP listener. A CPU-only library cannot provide CUDA acceleration. The API mode, base URL and API key fields apply to other providers, not native llama.cpp.

} {group==='performance'&&

Measure before tuning

Tokens/second measures generation speed; first-token delay measures how long a reply takes to start. llama.cpp supplies exact per-request stream timings, including reasoning tokens, without polling server-wide /metrics. Other providers may show an explicitly labelled estimate.

Applies immediately to full and mini chat. Hiding statistics does not change inference or logging.

Deferring Julia reduces competing analysis while the main model generates. Emotion and motion decisions may update later; an already-running call can still overlap. Main-model background slot priority is controlled separately in Models.

Logs go to logs/debug.log. DEBUG includes slot waits, prompt packing and Julia analysis time; INFO includes generation timings. Review logs before sharing: timing instrumentation does not collect conversation text, but other errors can contain local paths. Choose the level, size and backup count below, then Save and restart Python.

} {group==='graphics'&&
} diff --git a/tests/test_llama_server.py b/tests/test_llama_server.py new file mode 100644 index 0000000..00e99aa --- /dev/null +++ b/tests/test_llama_server.py @@ -0,0 +1,198 @@ +"""The external llama-server provider, against a fake server on a real loopback socket.""" +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import socket +import threading +import time + +import pytest + +from process.app_core.configuration.config import RuntimeConfig, load_config +from process.app_core.configuration.settings_store import SettingsStore, field +from process.app_core.conversation.messages import ChatMessage +from process.app_core.inference.llama_context import BackgroundPreempted +from process.app_core.inference import llama_server +from process.app_core.inference.llama_runtime import server_address +from process.app_core.inference.llama_server import LlamaServerProvider +from process.app_core.inference.providers import create_provider + +EVENTS = [ + {'type': 'response.output_text.delta', 'delta': 'Hello ', 'timings': {'predicted_n': 2, 'predicted_per_second': 20}}, + {'type': 'response.output_text.delta', 'delta': 'world'}, + {'type': 'response.completed', 'response': {'status': 'completed', 'output': [ + {'type': 'message', 'content': [{'type': 'output_text', 'text': 'Hello world'}]}, + {'type': 'function_call', 'call_id': 'call-1', 'name': 'lookup', 'arguments': '{"query":"color"}'}]}}] + + +class FakeServer: + """Speaks the llama-server routes Riko uses: /health, /slots, /apply-template, /tokenize, /v1/responses.""" + def __init__(self, slots=(8192, 8192), health=(200,), stall=None): # stall: None, 'prefill' or 'stream' + self.slots, self.health, self.stall = list(slots), list(health), stall + self.requests, self.disconnected, self.streaming = [], threading.Event(), threading.Event() + owner = self + class Handler(BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + def log_message(self, *args): pass + def reply(self, status, body): + data = json.dumps(body).encode() + self.send_response(status) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(data))) + self.end_headers() + self.wfile.write(data) + def wait_for_disconnect(self): + # Like llama-server: send nothing more, and stop as soon as the client goes away. + owner.streaming.set() + self.connection.settimeout(.05) + while owner.stall: + try: + if self.connection.recv(1, socket.MSG_PEEK) == b'': break + except socket.timeout: continue + except OSError: break + else: return False + owner.disconnected.set() + return True + def do_GET(self): + owner.requests.append((self.path, None, self.headers.get('Authorization'))) + if self.path == '/health': + status = owner.health.pop(0) if len(owner.health) > 1 else owner.health[0] + self.reply(status, {'status': 'ok' if status == 200 else 'loading model'}) + elif self.path == '/slots': self.reply(200, [{'id': i, 'n_ctx': n} for i, n in enumerate(owner.slots)]) + else: self.reply(404, {'error': {'message': 'not found'}}) + def do_POST(self): + body = json.loads(self.rfile.read(int(self.headers['Content-Length'])) or b'{}') + owner.requests.append((self.path, body, self.headers.get('Authorization'))) + if self.path == '/apply-template': return self.reply(200, {'prompt': 'rendered'}) + if self.path == '/tokenize': return self.reply(200, {'tokens': [1, 2, 3]}) + if self.path != '/v1/responses': return self.reply(404, {'error': {'message': 'not found'}}) + if not body.get('stream'): return self.reply(200, {'status': 'completed', 'output': []}) + self.close_connection = True + if owner.stall == 'prefill' and self.wait_for_disconnect(): return # silent before any header + self.send_response(200) + self.send_header('Content-Type', 'text/event-stream') + self.send_header('Connection', 'close') + self.end_headers() + try: + for index, event in enumerate(EVENTS): + self.wfile.write(f'event: {event["type"]}\ndata: {json.dumps(event)}\n\n'.encode()); self.wfile.flush() + if index == 0 and owner.stall == 'stream' and self.wait_for_disconnect(): return + except (BrokenPipeError, ConnectionResetError): owner.disconnected.set() + self.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) + self.server.daemon_threads = True + self.url = f'http://127.0.0.1:{self.server.server_address[1]}' + threading.Thread(target=self.server.serve_forever, daemon=True).start() + + def close(self): self.server.shutdown(); self.server.server_close() + + +def provider_for(url, **settings): + return LlamaServerProvider(RuntimeConfig(provider='llama_server', base_url=url, api_key='secret', n_ctx=8192, **settings)) + + +def test_llama_server_keeps_slots_exact_counts_tools_timings_and_auth(): + server = FakeServer() + provider = provider_for(server.url + '/v1/') # an OpenAI-style /v1 address works too + text, timing = [], [] + try: + result = provider.generate([ChatMessage('user', 'Hi')], on_delta=text.append, on_metrics=timing.append, + tools=[{'type': 'function', 'function': {'name': 'lookup', 'parameters': {'type': 'object'}}}]) + assert result.message.content == 'Hello world' and ''.join(text) == 'Hello world' + assert result.message.tool_calls[0].id == 'call-1' and result.message.tool_calls[0].arguments == {'query': 'color'} + assert timing == [{'predicted_n': 2, 'predicted_per_second': 20}] + assert provider.count_tokens([ChatMessage('user', 'Hi')]) == 3 + provider.reflection.generate([ChatMessage('user', 'Reflect')]) + provider.initiative.generate([ChatMessage('user', 'Consider')]) + provider.warmup() + finally: provider.close(); server.close() + payloads = [body for path, body, _ in server.requests if path == '/v1/responses'] + assert [body['id_slot'] for body in payloads] == [0, 1, 1, 0, 1] # live keeps slot 0; warmup touches each slot + assert all(body['cache_prompt'] for body in payloads) and payloads[0]['tools'][0]['name'] == 'lookup' + assert {'/health', '/slots', '/apply-template', '/tokenize'} <= {path for path, _, _ in server.requests} + assert all(auth == 'Bearer secret' for _, _, auth in server.requests) + + +def test_waits_while_the_model_loads_then_explains_a_slot_mismatch(): + server = FakeServer(slots=(2048, 2048), health=(503, 503, 200)) + provider = provider_for(server.url) + try: + with pytest.raises(RuntimeError) as error: provider.generate([ChatMessage('user', 'Hi')]) + assert '[2048, 2048]' in str(error.value) and '--parallel 2 --ctx-size 16384' in str(error.value) + assert [path for path, _, _ in server.requests].count('/health') == 3 + assert provider.client is None # nothing half-started; the next turn checks again + finally: provider.close(); server.close() + + +def test_a_missing_server_fails_fast_with_the_launch_command(): + with socket.socket() as probe: + probe.bind(('127.0.0.1', 0)); port = probe.getsockname()[1] + provider = provider_for(f'http://127.0.0.1:{port}', startup_timeout_seconds=600) + started = time.monotonic() + try: + with pytest.raises(RuntimeError, match=f'No llama-server is answering.*--port {port} --parallel 2'): provider.warmup() + assert time.monotonic() - started < 5 + finally: provider.close() + + +@pytest.mark.parametrize('stall', ['prefill', 'stream']) +def test_cancelling_while_the_server_is_silent_stops_at_once_and_frees_the_slot(stall): + server = FakeServer(stall=stall) + provider = provider_for(server.url) # request_timeout_seconds stays 120: cancelling must not wait for it + stop = threading.Event() + threading.Thread(target=lambda: server.streaming.wait(10) and stop.set(), daemon=True).start() + try: + started = time.monotonic() + with pytest.raises(BackgroundPreempted): provider.generate([ChatMessage('user', 'Hi')], on_delta=lambda _: None, cancelled=stop.is_set) + assert time.monotonic() - started < 5 and server.disconnected.wait(5) # llama-server stops that request too + server.stall = None + assert provider.generate([ChatMessage('user', 'Again')]).message.content == 'Hello world' # slot 0 was released + finally: provider.close(); server.close() + + +def test_a_server_that_goes_away_is_checked_again_and_named_in_the_error(): + server = FakeServer() + provider = provider_for(server.url) + try: + assert provider.generate([ChatMessage('user', 'Hi')]).message.content == 'Hello world' + server.close() + with pytest.raises(RuntimeError, match='No llama-server is answering at http://127.0.0.1.*--parallel 2'): provider.generate([ChatMessage('user', 'Again')]) + assert provider.client is None # the next turn re-runs /health and /slots + finally: provider.close() + + +def test_an_unreachable_host_fails_within_the_connect_timeout(monkeypatch): + monkeypatch.setattr(llama_server, 'CONNECT_TIMEOUT', .3) + provider = provider_for('http://10.255.255.1:8080', startup_timeout_seconds=600) # not routable: no answer at all + started = time.monotonic() + try: + with pytest.raises(RuntimeError, match='No llama-server is answering'): provider.warmup() + assert time.monotonic() - started < 5 + finally: provider.close() + + +def test_configuration_settings_and_probe_rules(tmp_path): + path = tmp_path / 'character_config.yaml' + path.write_text('runtime:\n provider: llama_server\n n_ctx: 8192\n', encoding='utf-8') + config = load_config(path) # no GGUF, native library or GPU settings: the server owns them + assert config.runtime.base_url == 'http://127.0.0.1:8080' + provider = create_provider(config.runtime) + try: assert isinstance(provider, LlamaServerProvider) and not provider.supports_latent_probe + finally: provider.close() + path.write_text('runtime:\n provider: llama_server\nemotion:\n enabled: true\n probe:\n enabled: true\n', encoding='utf-8') + with pytest.raises(ValueError, match='llama_cpp'): load_config(path) + path.write_text('runtime:\n provider: llama_server\n base_url: http://127.0.0.1:8080\n', encoding='utf-8') + store = SettingsStore(path) + paths = {item['path'] for item in store.snapshot()['fields']} + assert {'runtime.base_url', 'runtime.api_key', 'runtime.parallel_slots'} <= paths and 'runtime.api_mode' not in paths + assert store.validate({'runtime.temperature': .5}) == {'valid': True, 'errors': {}} # the minimal config stays editable + assert not store.validate({'runtime.base_url': 'http://127.0.0.1:8080/llama'})['valid'] # rejected before it is saved + assert 'llama_server' in field('runtime.provider', 'llama_server')['options'] + path.write_text('runtime:\n provider: llama_cpp\n model_path: model.gguf\n', encoding='utf-8') + paths = {item['path'] for item in SettingsStore(path).snapshot()['fields']} + assert {'runtime.base_url', 'runtime.api_key'} <= paths # switching to llama_server can set the address in the same draft + for text, problem in [('base_url: http://127.0.0.1:8080/llama', 'llama-server address'), ('base_url: http://user:pw@host:8080', 'llama-server address'), + ('api_key: 123456', 'quotes')]: + path.write_text(f'runtime:\n provider: llama_server\n {text}\n', encoding='utf-8') + with pytest.raises(ValueError, match=problem): load_config(path) + path.write_text('runtime:\n provider: llama_server\n base_url:\n', encoding='utf-8') + assert load_config(path).runtime.base_url == 'http://127.0.0.1:8080' + assert server_address(' http://127.0.0.1:8080/v1/ ') == 'http://127.0.0.1:8080' From e91c2cac02a15a8a086c2699fb31887e856d9338 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 16:06:14 +0530 Subject: [PATCH 08/37] Run on Metal: ASR factory, no FAISS, torch devices, llama.cpp options, clean shutdown Audit wave 2, verified on an Apple Silicon Mac with the native Metal bridge. - ASR (V013): one faster-whisper factory, audio/asr.py. auto/default resolve to what CTranslate2 can run here (CPU int8 on macOS); an unsupported pair in an existing config falls back once with a warning, and Settings names the value that cannot run when the pair is edited. Packaged first run writes cpu/int8. - Memory (V007): the semantic index is a numpy float32 matrix instead of FAISS, whose bundled libomp killed the backend beside torch's on macOS (reproduced with one memory and warmup off). Retrieval is identical; the index file is no longer written, and faiss-cpu is gone. - Torch devices (V090): runtime/torch_device.py (auto/cpu/cuda[:n]/mps), Julia maps mps to CPU, retries a failed GPU load on CPU, and no longer leaves torch capped at 4 threads; new memory.device setting. - llama.cpp options (V176, V211): flash_attn auto/on/off (default auto; YAML booleans keep their meaning), n_gpu_layers -2 for all layers and --fit only for -1, row split refused with an actionable message, budgets above the GGUF training context rejected before loading. riko-native returns llama.cpp's own WARN/ERROR lines on failure and offload/flash-attention notes on success; the probe identity records what flash attention resolved. - Shutdown (V162, V094): the active turn is cancelled before uvicorn drains (2 s), Discord stops in parallel with the session close, live native contexts are destroyed at exit or skipped with os._exit so ggml-metal's static destructor cannot abort, managed mode exits itself at 14 s, and Electron escalates SIGTERM to SIGKILL. Ctrl+C no longer prints a traceback. - Environment (V133, V117, V122): install_reqs.sh is strict and picks torch wheels by platform, NVIDIA driver and compute capability (ROCm with rocminfo), preferring the repo .venv; pyproject gains a runtime extra kept equal to requirements-runtime.txt; MCP commands resolve on the child PATH and load failures reach the desktop; the packaged macOS backend gets the Homebrew PATH; only the popup global shortcut is bound by default. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 2 + CLAUDE.md | 16 +- Code/desktop_server.py | 15 +- Code/process/app_core/audio/asr.py | 47 ++++++ Code/process/app_core/audio/voice_input.py | 8 +- Code/process/app_core/configuration/config.py | 6 +- .../app_core/configuration/settings_store.py | 38 +++-- Code/process/app_core/emotion/julia.py | 30 +++- .../app_core/inference/llama_context.py | 13 +- .../app_core/inference/llama_native.py | 81 ++++++++-- .../app_core/inference/llama_runtime.py | 31 +++- .../app_core/integrations/discord/api.py | 7 +- Code/process/app_core/persistence/memory.py | 56 ++++--- .../app_core/resources/vram_estimate.py | 16 +- Code/process/app_core/runtime/lifecycle.py | 18 ++- Code/process/app_core/runtime/torch_device.py | 44 ++++++ Code/process/app_core/runtime/warmup.py | 6 +- Code/process/app_core/tools/registry.py | 18 ++- Code/run_server.py | 25 ++- README.md | 12 +- electron/main.cjs | 8 +- electron/release.cjs | 15 +- electron/shortcuts.cjs | 10 ++ electron/src/discord_notifications.jsx | 4 +- electron/src/plain_language.mjs | 4 +- electron/src/release.test.mjs | 13 +- electron/src/settings_model.mjs | 10 +- electron/src/settings_model.test.mjs | 7 +- electron/src/settings_page.jsx | 2 +- electron/src/shortcuts.test.mjs | 23 +++ install_reqs.sh | 66 ++++++-- pyproject.toml | 10 +- requirements-runtime.txt | 18 +-- tests/test_asr.py | 96 ++++++++++++ tests/test_backend_shutdown.py | 36 +++++ tests/test_dependency_manifests.py | 21 +++ tests/test_desktop_api.py | 34 +++++ tests/test_gpu_resources.py | 23 ++- tests/test_install_script.py | 54 +++++++ tests/test_llama_native.py | 144 +++++++++++++++++- tests/test_llama_runtime.py | 43 +++++- tests/test_llama_slots.py | 2 + tests/test_memory_pipeline.py | 71 +++++++++ tests/test_settings_store.py | 59 +++++++ tests/test_tasks.py | 35 +++++ tests/test_torch_device.py | 78 ++++++++++ tools/llama_cpp/riko-native.cpp | 119 +++++++++++++-- tools/release/build.py | 2 +- 48 files changed, 1326 insertions(+), 170 deletions(-) create mode 100644 Code/process/app_core/audio/asr.py create mode 100644 Code/process/app_core/runtime/torch_device.py create mode 100644 electron/shortcuts.cjs create mode 100644 electron/src/shortcuts.test.mjs mode change 100644 => 100755 install_reqs.sh create mode 100644 tests/test_asr.py create mode 100644 tests/test_backend_shutdown.py create mode 100644 tests/test_dependency_manifests.py create mode 100644 tests/test_install_script.py create mode 100644 tests/test_torch_device.py diff --git a/.gitattributes b/.gitattributes index dfe0770..51dfd78 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # Auto detect text files and perform LF normalization * text=auto +# Shell scripts must keep LF on Windows checkouts, or bash rejects "set -o pipefail\r". +*.sh text eol=lf diff --git a/CLAUDE.md b/CLAUDE.md index 8e6271f..f430e67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,11 +15,11 @@ This layout is on `main` (formerly the `rewrite-2` branch). `origin/llama.cpp` i No linter or formatter is configured. Besides tests, the README's only checks are `node --check main.cjs` and `node --check preload.cjs` in `electron/`. ```bash -# Python >= 3.11 (CI uses 3.11). Tests need the full runtime deps; `pip install -e .[test]` alone fails collection. +# Python >= 3.11 (CI uses 3.11). Tests need the full runtime deps; the [test] extra pulls the [runtime] extra, which tests/test_dependency_manifests.py keeps equal to requirements-runtime.txt. python -m pip install -r requirements-runtime.txt python -m pip install --no-deps EfficientWord-Net # Linux CI pre-installs CPU torch: python -m pip install torch --index-url https://download.pytorch.org/whl/cpu -# install_reqs.sh pins CUDA 13 (cu130) torch wheels, which do not exist for macOS. +# install_reqs.sh (bash, set -euo pipefail) picks torch wheels per machine: PyPI on macOS, cu130/cu126 by NVIDIA driver, rocm7.2 with rocminfo, else cpu; RIKO_TORCH_INDEX overrides. python -u Code/run_server.py # backend; run from the repo root; Ctrl+C stops it @@ -71,7 +71,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. - The FastAPI lifespan calls `create_chat_service` (`app_core/factory.py`), which builds everything inside an `ExitStack` that rolls back on failure: `ActionController` → optional Julia emotion engine → `create_provider` (+ optional emotion probe) → `provider.warmup()` (loads the GGUF, no timeout) → `TaskStore` / `ToolRegistry` → `MemoryStore` → `ChatService`. Then come `SessionManager`, `warm_session` (ASR/VAD/wake/TTS, bounded by `runtime.startup_timeout_seconds`), `ConversationStore`, `Initiative` and the resource/task-file watchers, and finally `runtime.ready` is published. - `run_server.py` binds 127.0.0.1:8765 before the model loads (retrying for 20 s while a previous backend exits) and mints the API secrets right after; connections queue until lifespan startup finishes, so requests hang rather than fail while the model loads. - With `desktop.setup_on_startup_error: true`, a failing `create_chat_service` leaves only the settings/status/resource/media routes up (the rest return 503) so Settings can repair the YAML. -- Shutdown: stop Discord → `close_bounded(session, 10)` (`app_core/runtime/lifecycle.py`; a hung close is abandoned to a daemon thread) → unwind the ExitStack. +- Shutdown: `run_server.Server.shutdown` first cancels the active turn (`desktop_server.stop_turn`, bounded 1 s), then uvicorn drains for 2 s; the lifespan stops Discord in parallel with `close_bounded(session, 8)` (`app_core/runtime/lifecycle.py`; a hung close is abandoned to a daemon thread) → unwind the ExitStack. An atexit guard in `llama_native.py` destroys live native contexts, or `os._exit`s when one cannot be destroyed, because ggml-metal's static destructor aborts on leftover Metal buffers. The whole sequence stays under Electron's 15 s. ### Threading and events - `event_bus` (`app_core/events/bus.py`) is synchronous: listeners run on the publisher's thread, their exceptions are swallowed, and every event carries a monotonic `sequence`. @@ -85,7 +85,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. - `llama_server` → `LlamaServerProvider` (`llama_server.py`): the same `LlamaContextProvider` protocol (slot 0 for live, exact `/apply-template` + `/tokenize` counts, `/v1/responses` streaming and tools) against a llama-server the user runs at `runtime.base_url` (default `http://127.0.0.1:8080`; validated by `server_address` in `llama_runtime.py`). It uses `http.client`, one connection per request, and cancels by shutting the socket down (closing alone does not wake a read blocked during prefill). Startup waits on `/health` (503 while loading), then requires exactly `parallel_slots` slots each holding the largest of the live, initiative and reflection budgets, and prints the matching `llama-server` command otherwise; a connection failure resets it so the next request checks again. The server owns the model and GPU settings, and there is no emotion probe. - `openai`, `lm_studio`, `openai_compatible`, `ollama`, `local_http` → `OpenAIProvider`: Responses API with a Chat Completions fallback, byte-based token estimates, no slots, no provider-level cancel. - `SlotScheduler` (`llama_context.py`) reserves slot 0 for the `live` lane. `initiative` and `reflection` share slots 1..N-1 (`runtime.parallel_slots`, 2–4), and live turns preempt them when `runtime.pause_background_on_live` is set. The emotion probe captures only on slot 0. -- `--ctx-size` comes from `kv_budget.pool_capacity`. Per-role budgets are enforced only by Python-side packing (`context_budget.pack_context`, with exact native token counts) and by `max_output_tokens`. +- `--ctx-size` comes from `kv_budget.pool_capacity`. `runtime.flash_attn` is `auto`/`on`/`off` (YAML booleans mean on/off); `n_gpu_layers` -1 lets llama.cpp `--fit` keep 1 GiB free, -2 requires every layer, and only -1 passes `--fit on`. `split_mode: row` loads but the native provider refuses it. Budgets above the GGUF's training context fail before loading, and `riko_create` returns llama.cpp's own WARN/ERROR lines on failure and its offload/flash-attention notes on success. Per-role budgets are enforced only by Python-side packing (`context_budget.pack_context`, with exact native token counts) and by `max_output_tokens`. - Turn flow: `SessionManager.respond` → `ChatService.respond` (`app_core/conversation/chat.py`): 1. memory capture and recall; 2. the prompt: a stable system + history prefix (so the KV cache is reused), then `context_kind='optional'` system messages; @@ -108,7 +108,7 @@ Release (CI only, `.github/workflows/release.yml`): `python tools/release/build. The data root is the directory holding `character_config.yaml`. - `persistent_memories/` is user data: never delete it during cleanup. Runtime files: - `chat_history.json`: the model's context. Both `ChatService` and `SessionManager` rewrite the whole file (fsynced). An unreadable file is kept as `chat_history.json.unreadable-` before anything replaces it. Only a turn still generating or playing is cut by Stop/Sleep/shutdown; failed and cancelled turns keep the user's message. - - `memory_store.json` (`app_core/persistence/memory.py`): written via temp file, fsync and replace; fails closed. Its FAISS index is written but rebuilt from embeddings on load. + - `memory_store.json` (`app_core/persistence/memory.py`): written via temp file, fsync and replace; fails closed. Its semantic index is an in-memory float32 numpy matrix, re-embedded from record text on every start and never written (FAISS was dropped: its libomp crashed beside torch's on macOS); `memory.index_file` is ignored. - `conversations.sqlite3`: the UI archive, built from bus events. It is a second history and can diverge from `chat_history.json`. - `tasks.sqlite3` (`app_core/persistence/tasks.py`): revision-checked, and shared with `Code/task_mcp_server.py`. - The whiteboard, desktop, initiative, tool-approval and Discord JSON files, plus `wake_words/`. @@ -149,7 +149,7 @@ The data root is the directory holding `character_config.yaml`. - **Packaged, later launches:** - Electron spawns `backend/riko-backend` with `RIKO_MANAGED=1`, `RIKO_DATA_DIR`, `RIKO_CONFIG` and `RIKO_BUNDLE_ROOT`. - Hugging Face and torch caches go under `/models`. - - A `shutdown` line or EOF on stdin stops the backend; Electron kills it after 15 s. + - A `shutdown` line or EOF on stdin stops the backend (it dumps stacks and exits itself after 14 s); Electron sends SIGTERM after 15 s and SIGKILL 3 s later. On macOS the backend's PATH gets the Homebrew prefixes so MCP servers and ffmpeg resolve (`finderPath`). - `runtime.native_library: bundled:` resolves to `$RIKO_BUNDLE_ROOT/native//`. ## Conventions and gotchas @@ -179,6 +179,6 @@ Backend and device assumptions live here: - **Bundled-backend allowlists and library names:** `app_core/configuration/config.py`, `electron/release.cjs` (detects GPUs via nvidia-smi and vulkaninfo) and `electron/src/first_setup.jsx`. They are asserted by `tests/test_release_config.py` and `electron/src/release.test.mjs`. - **Library loading:** `os.add_dll_directory` is called only on Windows (`app_core/inference/llama_native.py`, `Code/run_server.py`). - **NVIDIA-only telemetry:** `app_core/resources/gpu_memory.py` (nvidia-smi, DXGI vendor `0x10de`, PowerShell counters) and the CUDA rows in `app_core/resources/vram_estimate.py`. -- **ASR defaults:** `device='cuda'` and `compute_type='int8_float16'` are set in `app_core/audio/voice_input.py`, `app_core/runtime/warmup.py`, `app_core/integrations/discord/api.py` and `app_core/configuration/settings_store.py`. They fail on macOS, where CTranslate2 has no GPU backend; `auto` + `default` works on CPU. -- **Julia devices:** the allowlist is `cpu`/`cuda`/`cuda:0`, with no `mps`. It lives in `app_core/emotion/julia.py` and the settings `ENUMS`, and `tests/test_llama_native.py` asserts it. +- **ASR:** every faster-whisper model is built by `app_core/audio/asr.py:create_whisper`. Defaults are `auto`/`default`, resolved from what CTranslate2 reports (CPU int8 on macOS, which has no CTranslate2 GPU backend); an unsupported configured pair falls back once with a warning, and Settings refuses one when it is edited. GPU ASR on Metal or ROCm needs a different engine (whisper.cpp, mlx-whisper, or a HIP-built CTranslate2). +- **Torch devices:** `app_core/runtime/torch_device.py` accepts `auto`/`cpu`/`cuda[:n]`/`mps` (ROCm torch reports `cuda`). Julia maps `mps` to CPU until upstream moves inputs for MPS, retries a failed GPU load on CPU, and restores torch's thread count after loading. `memory.device` covers the memory classifier and embedder; the settings `ENUMS` and `tests/test_llama_native.py` pin the lists. - **Windows-only behaviour:** `WindowsActivity` in `app_core/runtime/initiative.py`, the Win32 pointer hooks in `electron/main.cjs`, and the acrylic material in `electron/window_material.cjs`. diff --git a/Code/desktop_server.py b/Code/desktop_server.py index 2a048c3..d1618be 100644 --- a/Code/desktop_server.py +++ b/Code/desktop_server.py @@ -24,7 +24,7 @@ from process.app_core.persistence.tasks import TaskConflict from process.app_core.events.stream import stream_events from process.app_core.persistence.conversation_store import ConversationStore -from process.app_core.runtime.lifecycle import close_bounded +from process.app_core.runtime.lifecycle import close_bounded, run_bounded from process.app_core.desktop.media import resolve_media from fastapi.responses import FileResponse @@ -114,15 +114,24 @@ async def lifespan(_app): event_bus.publish("runtime.ready", provider=config.runtime.provider) yield finally: - await run_in_threadpool(discord_launcher.stop) + # Electron allows the whole shutdown 15 s: run_server spends at most 1 s stopping the turn and 2 s draining + # requests, this block at most about 8.5 s, and llama_native 2 s destroying the native context at exit. + discord = asyncio.create_task(run_in_threadpool(discord_launcher.stop)) # a separate process; stop it meanwhile if task_file_events: task_file_events.close(); task_file_events = None if resource_events: resource_events.close(); resource_events = None - if session: await run_in_threadpool(close_bounded, session, 10) + if session: await run_in_threadpool(close_bounded, session, 8) elif chat: await run_in_threadpool(close_bounded, chat, 6) + await discord for unsubscribe in reversed(unsubscribers): unsubscribe() if conversation_store: conversation_store.close() event_bus.publish("runtime.stopped") +def stop_turn(): + """run_server calls this before uvicorn drains requests, so an in-flight chat returns now instead of generating + through the drain. Bounded: a stuck cancel must not hold up the rest of shutdown.""" + cancel = getattr(session, 'cancel', None) + if cancel and not getattr(session, '_closed', False): run_bounded(cancel, 1, 'SessionManager.cancel') + _dev = os.environ.get('RIKO_DEV') == '1' # The API map is published only while developing. app = FastAPI(title="Riko Local Desktop Runtime", lifespan=lifespan, docs_url='/docs' if _dev else None, redoc_url='/redoc' if _dev else None, openapi_url='/openapi.json' if _dev else None) diff --git a/Code/process/app_core/audio/asr.py b/Code/process/app_core/audio/asr.py new file mode 100644 index 0000000..6e1ac6c --- /dev/null +++ b/Code/process/app_core/audio/asr.py @@ -0,0 +1,47 @@ +"""The one faster-whisper factory. auto/default resolve to what this CTranslate2 build can run here (it has no +Metal backend, and its CPU path rejects float16), and a configured pair it cannot run falls back once, with one +warning, instead of failing every utterance.""" +from functools import lru_cache +import logging + +logger = logging.getLogger(__name__) +MODEL = 'distil-small.en' +# Fastest first, matching CTranslate2's own 'auto'. CPU 'int8' runs as int8_float32. +PREFERRED = {'cuda': ('int8_float16', 'int8_float32', 'float16', 'float32'), 'cpu': ('int8', 'float32')} +_warned = set() + + +@lru_cache(maxsize=1) +def supported_types(): + """{device: compute types} that this CTranslate2 build supports; 'cuda' only with a visible device.""" + try: import ctranslate2 + except ImportError: return {'cpu': frozenset({'float32'})} + types = {'cpu': frozenset(ctranslate2.get_supported_compute_types('cpu'))} + try: + if ctranslate2.get_cuda_device_count() > 0: types['cuda'] = frozenset(ctranslate2.get_supported_compute_types('cuda')) + except Exception as exc: logger.warning('CTranslate2 CUDA probe failed; speech recognition stays on the CPU: %s', exc) + return types + + +def resolve(voice): + """(device, compute_type, note) to construct with; note says why a configured pair was replaced, else ''.""" + wanted = str(voice.get('asr_device') or 'auto'), str(voice.get('asr_compute_type') or 'default') + supported, (device, precision), notes = supported_types(), wanted, [] + if device == 'auto': device = 'cuda' if 'cuda' in supported else 'cpu' + if device not in supported: + notes.append('no CUDA device is visible to CTranslate2' if device == 'cuda' else f'unknown device {device}') + device = 'cpu' + best = next((name for name in PREFERRED[device] if name in supported[device]), 'float32') + if precision in ('default', 'auto'): precision = best + elif precision not in supported[device]: notes.append(f'{device} does not support {precision}'); precision = best + return device, precision, '; '.join(notes) + + +def create_whisper(voice): + from faster_whisper import WhisperModel + device, precision, note = resolve(voice) + if note and note not in _warned: + _warned.add(note) + logger.warning('Speech recognition %s/%s cannot run here (%s); using %s/%s. Choose auto and default in Settings.', + voice.get('asr_device', 'auto'), voice.get('asr_compute_type', 'default'), note, device, precision) + return WhisperModel(voice.get('asr_model', MODEL), device=device, compute_type=precision) diff --git a/Code/process/app_core/audio/voice_input.py b/Code/process/app_core/audio/voice_input.py index f5886a2..12b3ab9 100644 --- a/Code/process/app_core/audio/voice_input.py +++ b/Code/process/app_core/audio/voice_input.py @@ -1,4 +1,4 @@ -"""Python capture, VAD, incremental GPU ASR and turn dispatch are separate workers.""" +"""Python capture, VAD, incremental ASR and turn dispatch are separate workers.""" import logging import queue import threading @@ -7,6 +7,7 @@ from ..runtime.cancellation import TurnCancelled from ..events.bus import event_bus +from .asr import create_whisper from .voice_segments import VoiceSegments logger = logging.getLogger(__name__) @@ -148,7 +149,6 @@ def _asr(self): self._parts.pop(segment.utterance_id, None) continue import numpy as np - from faster_whisper import WhisperModel config = self.session.config.raw.get("voice", {}) if not segment.provisional: event_bus.publish('voice.transcribing', utterance_id=segment.utterance_id) parts = self._parts.setdefault(segment.utterance_id, []) @@ -159,9 +159,7 @@ def _asr(self): if self.model is None: self.model = getattr(self.session, 'warmed_asr', None) if self.model is None: - self.model = WhisperModel(config.get("asr_model", "distil-small.en"), - device=config.get("asr_device", "cuda"), compute_type=config.get("asr_compute_type", "int8_float16")) - self.session.warmed_asr = self.model + self.model = self.session.warmed_asr = create_whisper(config) segments, _ = self.model.transcribe(audio, beam_size=1, vad_filter=False, condition_on_previous_text=False) text = " ".join(item.text.strip() for item in segments).strip() diff --git a/Code/process/app_core/configuration/config.py b/Code/process/app_core/configuration/config.py index 9dd1eb6..e708264 100644 --- a/Code/process/app_core/configuration/config.py +++ b/Code/process/app_core/configuration/config.py @@ -37,7 +37,7 @@ class RuntimeConfig: n_ubatch: int = 512 n_threads: int | None = None n_threads_batch: int | None = None - flash_attn: bool = False + flash_attn: str | bool = 'auto' # auto, on or off; YAML true/false still mean on/off type_k: str = 'f16' type_v: str = 'f16' offload_kqv: bool = True @@ -75,7 +75,7 @@ class MemoryConfig: history_file: Path = Path("persistent_memories/chat_history.json") context_window_tokens: int = 8192 store_file: Path = Path("persistent_memories/memory_store.json") - index_file: Path = Path("persistent_memories/faiss_index.index") + index_file: Path = Path("persistent_memories/faiss_index.index") # legacy: no longer written or read embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2" embedding_dimension: int = 384 max_results: int = 8 @@ -89,6 +89,7 @@ class MemoryConfig: system1_model_id: str = "SupersonicLabs/Julia-1" system1_cache_dir: Path | None = None system1_max_length: int = 8192 + device: str = "cpu" minimum_importance: float = 0.35 default_memories: list[dict[str, Any]] = field(default_factory=list) @@ -248,6 +249,7 @@ def load_config(path: str | Path | None = None) -> AppConfig: system1_model_id=str(memory_raw.get("system1_model_id", "SupersonicLabs/Julia-1")), system1_cache_dir=_path(root, memory_raw.get("system1_cache_dir")), system1_max_length=int(memory_raw.get("system1_max_length", 8192)), + device=str(memory_raw.get("device", "cpu")), minimum_importance=float(memory_raw.get("minimum_importance", 0.35)), default_memories=list(memory_raw.get("default_memories", preset.get("memories", []))), ), diff --git a/Code/process/app_core/configuration/settings_store.py b/Code/process/app_core/configuration/settings_store.py index e2be747..63b2b35 100644 --- a/Code/process/app_core/configuration/settings_store.py +++ b/Code/process/app_core/configuration/settings_store.py @@ -17,16 +17,17 @@ OBSOLETE = {'avatar.camera.distance', 'avatar.expression_engine', 'avatar.view', 'desktop.shortcuts.effects', 'emotion.pause_during_inference', 'runtime.server_path', 'emotion.temperature', 'sovits_ping_config.media_type', 'your_name', - 'model','base_url','api_key','tokenizer_model'} + 'model','base_url','api_key','tokenizer_model', 'memory.index_file'} ENUMS = { 'logging.level': ['DEBUG','INFO','WARNING','ERROR'], 'runtime.provider': ['llama_cpp', 'llama_server', 'lm_studio', 'openai', 'openai_compatible', 'ollama', 'local_http'], 'runtime.api_mode': ['auto', 'responses', 'chat_completions'], 'runtime.type_k': ['f16', 'f32', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl'], 'runtime.type_v': ['f16', 'f32', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl'], - 'runtime.split_mode': ['none', 'layer', 'row'], 'voice.mode': ['wake_word', 'continuous', 'manual'], - 'voice.asr_device': ['cuda', 'cpu', 'auto'], 'voice.asr_compute_type': ['int8_float16', 'float16', 'int8', 'float32', 'default'], - 'emotion.device': ['cpu', 'cuda', 'cuda:0'], 'avatar.format': ['auto', 'vrm0', 'vrm1'], 'avatar.view': ['full_body'], + 'runtime.split_mode': ['none', 'layer'], 'runtime.flash_attn': ['auto', 'on', 'off'], 'voice.mode': ['wake_word', 'continuous', 'manual'], + 'voice.asr_device': ['auto', 'cuda', 'cpu'], 'voice.asr_compute_type': ['default', 'int8_float16', 'float16', 'int8', 'float32'], + 'emotion.device': ['cpu', 'auto', 'cuda', 'cuda:0'], 'memory.device': ['cpu', 'auto', 'cuda', 'cuda:0', 'mps'], + 'avatar.format': ['auto', 'vrm0', 'vrm1'], 'avatar.view': ['full_body'], 'sovits_ping_config.text_lang': ['en', 'zh', 'ja', 'ko', 'yue', 'auto'], 'sovits_ping_config.prompt_lang': ['en', 'zh', 'ja', 'ko', 'yue', 'auto'], 'sovits_ping_config.media_type': ['raw'], @@ -35,7 +36,7 @@ 'logging.max_mb': (1,100), 'logging.backups': (1,10), 'emotion.probe.interval_tokens': (1, 512), 'runtime.parallel_slots': (2, 4), 'runtime.n_ctx': (1, 1048576), - 'runtime.n_gpu_layers': (-1, 1000), 'runtime.n_batch': (1, 65536), 'runtime.n_ubatch': (1, 65536), + 'runtime.n_gpu_layers': (-2, 1000), 'runtime.n_batch': (1, 65536), 'runtime.n_ubatch': (1, 65536), 'runtime.n_threads': (1, 1024), 'runtime.n_threads_batch': (1, 1024), 'runtime.request_timeout_seconds': (.1, 86400), 'runtime.startup_timeout_seconds': (1, 86400), 'runtime.cache_size_mb': (0, 1048576), 'runtime.main_gpu': (0, 128), @@ -94,7 +95,8 @@ 'logging.level': 'DEBUG includes detailed application diagnostics; INFO includes inference timings; WARNING and ERROR restrict output. Restart Python to apply.', 'logging.inference_timings': 'Record provider duration, first-token latency and output rate without recording prompts or responses. Restart Python to apply.', 'runtime.native_library': 'Required for llama_cpp: a compatible riko-native library. Runs llama.cpp inside Python, including Responses, tools, streaming, cancellation and probe capture. No server process or HTTP listener. Use a CUDA-enabled build for GPU acceleration. Save and restart Python.', - 'emotion.device': 'Julia-1 teacher device: CPU by default; CUDA is opt-in and requires a compatible GPU torch/native Julia runtime. The latent probe always runs on CPU.', + 'emotion.device': 'Julia-1 teacher device: CPU by default, keeping GPU memory for the main model. CUDA is opt-in and needs a GPU build of torch (ROCm builds also report cuda); auto uses it when available. Julia does not support Apple MPS yet, so Macs use CPU. A GPU that fails to load falls back to CPU. The latent probe always runs on CPU. Restart Python.', + 'memory.device': 'Device for the memory classifier (Julia-1) and the embedding model. CPU by default. auto picks CUDA, then Apple MPS, then CPU. The classifier stays on CPU under MPS; embeddings can use it. GPU use competes with the main model for memory, and a GPU that fails to load falls back to CPU. Restart Python.', 'emotion.probe.enabled': 'Train a read-only CPU emotion probe against genuine Julia-1 labels from native llama.cpp hidden states. Requires the custom in-process DLL. Julia remains fallback until held-out validation passes.', 'emotion.probe.interval_tokens': 'Run native capture and the CPU emotion probe at most once per this many generated tokens (reasoning-only samples are excluded). Default 32. Applies immediately on save. Generation still evaluates every token. Smaller values increase capture and teacher work.', 'runtime.parallel_slots': 'One slot reserved for live replies. Other slots prioritize initiative over reflection.', @@ -110,12 +112,16 @@ 'runtime.hf_repo_id': 'Search Hugging Face, or paste an owner/repository ID. No download occurs until backend restart.', 'runtime.hf_filename': 'Choose an exact GGUF. For split models choose the first shard; not the mmproj file.', 'runtime.model_path': 'An explicit local GGUF takes precedence over Hugging Face.', - 'runtime.flash_attn': 'Hardware/build-dependent. Quantized V cache requires flash attention.', + 'runtime.flash_attn': 'auto lets llama.cpp use flash attention where the GPU backend supports it for this model. Quantized V cache needs auto or on (it forces flash attention on).', + 'runtime.n_gpu_layers': '-1 fits as many layers as free GPU memory allows at load time, keeping 1 GiB free (layers left over run on the CPU, slower). -2 requires every layer on the GPU. 0 or more is an exact layer count.', + 'runtime.split_mode': 'How a model is spread over several GPUs: layer gives each GPU whole layers; none uses only the primary GPU. Row split is not available in this llama.cpp build.', 'runtime.type_k': 'KV tensor quantization, independent of model weight quantization.', 'runtime.cache_size_mb': 'Optional extra RAM prompt cache; 0 still preserves per-slot live prefix reuse.', 'runtime.warmup': 'Preload models and silently test TTS. Does not open your microphone.', 'voice.input_device': 'Automatic uses the system microphone. Changes apply on backend restart.', 'voice.wake_word': 'One short name supported by EfficientWord-Net. Changing name/device requires its matching enrollment.', + 'voice.asr_device': 'auto uses CUDA when CTranslate2 sees a supported GPU (NVIDIA, or AMD with a HIP-built CTranslate2), otherwise the CPU. CTranslate2 has no Metal backend, so Macs transcribe on the CPU. Packaged builds bundle no CUDA libraries for speech recognition; keep cpu there. Save and restart Python.', + 'voice.asr_compute_type': 'default picks the fastest precision the device supports: int8_float16 on recent NVIDIA GPUs, int8 on the CPU. float16 and int8_float16 need a GPU. A pair this machine cannot run is refused here; one already in the file is replaced at startup with a logged warning. Save and restart Python.', 'voice.live_transcript_interval_seconds': 'Rolling provisional ASR updates while you are still speaking. One partial job at a time; final transcription replaces partial text. Smaller intervals increase ASR work. Applies on microphone restart.', 'tools.require_approval': 'Default for new/unconfigured tools. Per-tool live permissions override this default; approval bubbles expire after two minutes without executing.', 'tools.best_fit_inputs': 'Correct finite-choice desktop inputs before approval. Reuses the enabled, already-loaded CPU Julia model; safe spelling normalization is the fallback. Never repairs file permissions, numeric values or arbitrary paths.', @@ -146,7 +152,7 @@ 'runtime.native_library': 'In-process llama.cpp library', 'emotion.probe.interval_tokens': 'Emotion probe interval (tokens)', 'emotion.probe.enabled': 'Latent emotion probe', - 'emotion.device': 'Julia-1 compute device', + 'emotion.device': 'Julia-1 compute device', 'memory.device': 'Memory model device', 'runtime.provider':'Backend', 'runtime.model_path':'Local GGUF file', 'runtime.hf_repo_id':'Hugging Face model', 'runtime.hf_filename':'GGUF file', 'runtime.hf_revision':'Model revision', 'runtime.hf_local_files_only':'Offline mode', 'runtime.n_ctx':'Live context length (tokens)', @@ -211,6 +217,7 @@ def field(path, value): 'memory.reflection_context_window_tokens': 'Token budgets', 'memory.reflection_max_output_tokens': 'Token budgets', 'memory.reflection_enabled': 'Background models', 'memory.system1_enabled': 'Background models', 'memory.system1_model_id': 'Background models', 'memory.system1_cache_dir': 'Background models', 'memory.system1_max_length': 'Token budgets', + 'memory.device': 'Background models', 'memory.embeddings_enabled': 'Embedding model', 'memory.embedding_model': 'Embedding model', 'memory.embedding_dimension': 'Embedding model', } if path.startswith('emotion.'): resource_sections[path] = 'Emotion model' @@ -274,6 +281,7 @@ def _values(self, raw): if group == 'memory' and key in {'default_memories', 'history_file'}: continue values[f'{group}.{key}'] = value values.update(dict(flatten(raw))) + if type(values.get('runtime.flash_attn')) is bool: values['runtime.flash_attn'] = 'on' if values['runtime.flash_attn'] else 'off' for key,value in {'auto_start':False,'executable':None,'arguments':[]}.items(): values.setdefault('sovits_ping_config.'+key,value) for key,value in {'file_enabled':True,'level':'INFO','inference_timings':True,'max_mb':5,'backups':3}.items(): @@ -299,7 +307,8 @@ def _values(self, raw): values['initiative.context_window_tokens'] = candidate.runtime.initiative_n_ctx values['initiative.max_output_tokens'] = candidate.runtime.initiative_max_output_tokens if candidate.runtime.kv_pool_auto: values['runtime.kv_pool_tokens'] = candidate.runtime.kv_pool_tokens - for key, value in {'asr_model':'distil-small.en','asr_device':'cuda','asr_compute_type':'int8_float16','live_transcript_interval_seconds':2.0}.items(): + from ..audio.asr import MODEL + for key, value in {'asr_model':MODEL,'asr_device':'auto','asr_compute_type':'default','live_transcript_interval_seconds':2.0}.items(): values.setdefault('voice.' + key, value) return json.loads(json.dumps(values, default=str)) @@ -346,6 +355,16 @@ def prepare(self, changes): voice = candidate.raw.get('voice', {}) if voice.get('mode', 'wake_word') not in {'wake_word', 'manual', 'continuous'}: errors['voice.mode'] = 'Invalid activation mode' if len(str(voice.get('wake_word', 'Riko')).split()) != 1: errors['voice.wake_word'] = 'Use one short wake name' + # Check the speech recognition pair only when it is edited, so a pair this machine cannot run (which + # falls back at startup) never blocks saving other settings. + asr_keys = [key for key in ('voice.asr_device', 'voice.asr_compute_type') if key in changes] + if asr_keys: + from ..audio.asr import resolve + device, _, note = resolve(voice) + # Name the field whose value cannot run here, which may be the one the user did not edit. + for part in filter(None, note.split('; ')): + if ' does not support ' in part: errors['voice.asr_compute_type'] = f'{device} cannot run {voice.get("asr_compute_type")} here; choose default' + else: errors['voice.asr_device'] = f'Not available on this machine ({part}); choose auto or cpu' for path, value in dict(flatten(raw)).items(): if path.endswith(('_url', '.url')) and isinstance(value, str) and not value.startswith(('http://', 'https://')): errors[path] = 'Use an http:// or https:// URL' @@ -364,6 +383,7 @@ def prepare(self, changes): if rt.provider == 'llama_cpp': pool_capacity(rt) if rt.n_ctx <= 0: errors['runtime.n_ctx'] = 'Managed server requires a positive context' + if rt.split_mode == 'row': errors['runtime.split_mode'] = 'Row split is not available in this llama.cpp build; choose layer or none' budget = candidate.memory.context_window_tokens + rt.max_output_tokens if budget > rt.n_ctx: errors['runtime.n_ctx'] = f'Context must fit conversation budget + response ({budget} tokens)' except (ValueError, TypeError, KeyError) as exc: errors['__all__'] = str(exc) diff --git a/Code/process/app_core/emotion/julia.py b/Code/process/app_core/emotion/julia.py index 326d0aa..b00ff99 100644 --- a/Code/process/app_core/emotion/julia.py +++ b/Code/process/app_core/emotion/julia.py @@ -7,13 +7,31 @@ from pathlib import Path from typing import Callable +from ..runtime.torch_device import validate as validate_device from .models import EMOTIONS, EmotionEvent, EmotionState logger = logging.getLogger(__name__) +def load_julia(julia, source, device, **options): + """Load without leaking Julia's process-wide torch settings; a failed GPU load retries on CPU. + + Julia's FastEngine moves inputs to the device only for CUDA, so on MPS every predict fails + ('Passed CPU tensor to MPS op'): run it on CPU there until upstream supports MPS. + """ + from ..runtime.torch_device import preserve_torch_globals, resolve + target = resolve(device) + if target == 'mps': logger.info('Julia 1 does not support MPS yet; using CPU'); target = 'cpu' + with preserve_torch_globals(): + try: return julia.load_model(source, device=target, **options), target + except Exception as exc: + if target == 'cpu': raise + logger.warning('Julia 1 could not load on %s; retrying on CPU: %s', target, exc) + return julia.load_model(source, device='cpu', **options), 'cpu' + + class JuliaEmotionEngine: - """Iterative emotional interpreter for Julia 1; CPU by default, CUDA opt-in. + """Iterative emotional interpreter for Julia 1; CPU by default, CUDA (or auto) opt-in. The engine stores a rolling transcript made from user and assistant deltas. It evaluates that window after meaningful deltas rather than retaining the @@ -30,8 +48,7 @@ def __init__(self, model_path: str | Path | None, *, model_id: str = "Supersonic self.model_path = Path(model_path).expanduser() if model_path else None self.model_id = model_id self.cache_dir = Path(cache_dir).expanduser() if cache_dir else None - self.device = device - if device not in {'cpu', 'cuda', 'cuda:0'}: raise ValueError('Unsupported Julia device') + self.device, self.active_device = validate_device(device), None self.strict_encoding = strict_encoding self.max_length = max_length self.context_tokens = max(128, context_tokens) @@ -67,9 +84,8 @@ def _load_model(self): sys.path.insert(0, source) julia = import_module("julia") self._resolved_source = source - self._model = julia.load_model(source, device=self.device, strict_encoding=self.strict_encoding, - max_length=self.max_length, - head_length=max(128, self.max_length - self.context_tokens)) + self._model, self.active_device = load_julia(julia, source, self.device, strict_encoding=self.strict_encoding, + max_length=self.max_length, head_length=max(128, self.max_length - self.context_tokens)) from .compat import compatible_engine compatible_engine(self._model) except Exception as exc: @@ -168,7 +184,7 @@ def _interpret(self, stream: str, delta: str) -> EmotionState: try: self._trim_window() response = model.predict(state=self._transcript(), questions=self._questions()) - logger.debug('Julia interpretation duration_s=%.3f device=%s', time.perf_counter()-started, self.device) + logger.debug('Julia interpretation duration_s=%.3f device=%s', time.perf_counter()-started, self.active_device) return self._parse_julia_result(response, stream) except Exception as exc: logger.warning("Julia 1 interpretation failed: %s", exc) diff --git a/Code/process/app_core/inference/llama_context.py b/Code/process/app_core/inference/llama_context.py index 17d494d..2407ab1 100644 --- a/Code/process/app_core/inference/llama_context.py +++ b/Code/process/app_core/inference/llama_context.py @@ -9,7 +9,7 @@ import uuid import math -from .llama_runtime import validate_slots +from .llama_runtime import flash_attention, validate_slots logger = logging.getLogger(__name__) @@ -96,8 +96,9 @@ def native_arguments(config, model): args = ['riko-native', '--model', str(model), '--parallel', str(config.parallel_slots), '--ctx-size', str(context_capacity(config)), '--kv-unified' if config.kv_unified else '--no-kv-unified', '--cont-batching', '--jinja', '--slots', '--no-context-shift', - '--n-gpu-layers', str(config.n_gpu_layers), '--batch-size', str(config.n_batch), - '--ubatch-size', str(config.n_ubatch), '--flash-attn', 'on' if config.flash_attn else 'off', + # -1 lets llama.cpp's --fit lower the layer count to keep 1 GiB of GPU memory free; any other value is exact. + '--n-gpu-layers', str(config.n_gpu_layers), '--fit', 'on' if config.n_gpu_layers == -1 else 'off', + '--batch-size', str(config.n_batch), '--ubatch-size', str(config.n_ubatch), '--flash-attn', flash_attention(config.flash_attn), '--cache-type-k', config.type_k, '--cache-type-v', config.type_v, '--main-gpu', str(config.main_gpu), '--split-mode', config.split_mode, '--cache-ram', str(config.cache_size_mb), '--seed', str(config.seed)] @@ -163,12 +164,16 @@ def _initialize_probe(self, model): identity = {'gguf_sha256': digest.hexdigest(), 'server_build': props.get('build_info'), 'chat_template': props.get('chat_template'), 'feature_version': FEATURE_VERSION, 'type_k': self.config.type_k, 'type_v': self.config.type_v, - 'flash_attn': self.config.flash_attn, 'n_ctx': self.config.n_ctx, + 'flash_attn': self._flash_attention_identity(), 'n_ctx': self.config.n_ctx, 'runtime_fingerprint': self._probe_runtime_fingerprint()} self.probe = self.probe_factory(identity, self.probe_idle) def _probe_runtime_fingerprint(self): return None + def _flash_attention_identity(self): + # on/off keep the true/false of probe data recorded before flash_attn had an auto setting. + return {'on': True, 'off': False}.get(flash_attention(self.config.flash_attn), 'auto') + def set_foreground(self, active): self.scheduler.set_foreground(active) def set_pause_background(self, enabled): diff --git a/Code/process/app_core/inference/llama_native.py b/Code/process/app_core/inference/llama_native.py index a750e31..3954c94 100644 --- a/Code/process/app_core/inference/llama_native.py +++ b/Code/process/app_core/inference/llama_native.py @@ -4,6 +4,7 @@ chat/tool parsers. Transport-compatible response objects keep the application streaming/cancellation contract unchanged. All callbacks enqueue bytes only. """ +import atexit import ctypes import hashlib import json @@ -11,33 +12,65 @@ import os from pathlib import Path import queue +import re import threading import time from .llama_context import LlamaContextProvider, context_capacity, native_arguments -from .llama_runtime import resolve_model, validate_runtime +from .llama_runtime import check_native_build, resolve_model, validate_runtime +from ..runtime.lifecycle import close_bounded logger = logging.getLogger(__name__) OUTPUT = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p) CANCEL = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p) +# Every NativeRuntime from riko_create until riko_destroy returns; a None handle is still loading. ggml-metal keeps +# its devices in a C++ static whose destructor, run by exit(), asserts that every Metal buffer was already freed. +_LIVE = set() + + +def release_native(timeout): + """Destroy every live native context. False when one is still loading or does not stop in time.""" + runtimes = list(_LIVE) + if any(runtime.handle is None for runtime in runtimes): return False # riko_create cannot be interrupted + deadline = time.monotonic() + timeout + for runtime in runtimes: close_bounded(runtime, max(0, deadline - time.monotonic())) + while _LIVE and time.monotonic() < deadline: time.sleep(.02) + return not _LIVE + + +@atexit.register +def _release_at_exit(): + # Python's atexit runs before C++ static destructors. A context that cannot be destroyed in time is left to the + # OS instead: os._exit skips those destructors, so Metal cannot turn the exit into a GGML_ASSERT abort. + if not _LIVE: return + try: + import signal + if threading.current_thread() is threading.main_thread(): signal.signal(signal.SIGINT, signal.SIG_IGN) # a late Ctrl+C must not skip the fallback + if release_native(2): return + except BaseException: pass + logger.error('Native inference context still loading or in use at exit; leaving it to the OS') + logging.shutdown() + os._exit(1) class NativeRuntime: def __init__(self, library, arguments, interval=0): path = Path(library).resolve() + self.lock, self.requests, self.closed, self.handle = threading.Lock(), set(), False, None self.directory = os.add_dll_directory(str(path.parent)) if os.name == 'nt' else None + _LIVE.add(self) try: self.dll = ctypes.CDLL(str(path)) self._bind() - error = ctypes.create_string_buffer(4096) + error = ctypes.create_string_buffer(8192) self.handle = self.dll.riko_create(json.dumps(arguments).encode(), interval, error, len(error)) if not self.handle: raise RuntimeError(error.value.decode('utf-8', errors='replace')) + # On success the bridge reports layer offload and flash attention here (older builds leave it empty). + self.notes = error.value.decode('utf-8', errors='replace') except BaseException: + _LIVE.discard(self) if self.directory: self.directory.close() raise - self.lock = threading.Lock() - self.requests = set() - self.closed = False def _bind(self): self.dll.riko_create.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t] @@ -83,6 +116,7 @@ def close(self): def _destroy_after(self, requests): for request in requests: request.thread.join() self.dll.riko_destroy(self.handle) + _LIVE.discard(self) # before the handle clears: release_native reads a None handle as still loading self.handle = None if self.directory: self.directory.close() @@ -227,9 +261,19 @@ def post(self, path, json=None, **kwargs): def get(self, path, **kwargs): return self.post(path) +def training_context(model): + """The GGUF's {arch}.context_length: llama.cpp caps every slot's context there. None when unreadable.""" + from ..resources.vram_estimate import read_gguf + try: meta = read_gguf(model) + except (OSError, ValueError, UnicodeDecodeError): return None + value = meta.get(str(meta.get('general.architecture', '')) + '.context_length') + return value if type(value) is int and value > 0 else None + + class InProcessLlamaProvider(LlamaContextProvider): def __init__(self, config): validate_runtime(config) + check_native_build(config) context_capacity(config) super().__init__(config) self.native = None @@ -244,19 +288,31 @@ def _start(self): if not Path(self.config.native_library).is_file(): raise RuntimeError(f'runtime.native_library does not exist: {self.config.native_library}. Build or select a compatible riko-native library before loading the model.') model = resolve_model(self.config) + budgets = {'runtime.n_ctx': self.config.n_ctx, 'initiative.context_window_tokens': getattr(self.config, 'initiative_n_ctx', 4096), + 'memory.reflection_context_window_tokens': getattr(self.config, 'reflection_n_ctx', 4096)} + limit = training_context(model) + if limit and max(budgets.values()) > limit: + over = ', '.join(f'{key} ({value})' for key, value in budgets.items() if value > limit) + raise RuntimeError(f'This model was trained for {limit} tokens and llama.cpp caps each slot there. Lower {over} to {limit} or less.') args = native_arguments(self.config, model) native = NativeRuntime(self.config.native_library, args, self.probe_interval if self.probe_factory else 0) - logger.info('Inference transport=in_process requested_gpu_layers=%s threads=%s flash_attention=%s kv_k=%s kv_v=%s slots=%s probe_interval=%s', + notes = getattr(native, 'notes', '') + logger.info('Inference transport=in_process requested_gpu_layers=%s threads=%s flash_attention=%s kv_k=%s kv_v=%s slots=%s probe_interval=%s native=%r', self.config.n_gpu_layers, self.config.n_threads, self.config.flash_attn, self.config.type_k, self.config.type_v, - self.config.parallel_slots, self.probe_interval if self.probe_factory else 0) + self.config.parallel_slots, self.probe_interval if self.probe_factory else 0, notes) + offload = re.search(r'offloaded (\d+)/(\d+) layers', notes) + if offload and self.config.n_gpu_layers == -1 and int(offload[1]) < int(offload[2]): + logger.warning('llama.cpp put only %s of %s layers on the GPU to keep 1 GiB of GPU memory free; replies will be slower. ' + 'Free GPU memory, or set runtime.n_gpu_layers to -2 to require every layer on the GPU.', offload[1], offload[2]) self.native, self.client = native, NativeClient(native, self.config.request_timeout_seconds) try: response = self.client.get('/slots') self._check_response(response) slots = response.json() - required = max(self.config.n_ctx, getattr(self.config, 'initiative_n_ctx', 4096), getattr(self.config, 'reflection_n_ctx', 4096)) + required = max(budgets.values()) if len(slots) != self.config.parallel_slots or any(s['n_ctx'] < required for s in slots): - raise RuntimeError('Native context did not allocate the requested per-slot context') + raise RuntimeError(f'llama.cpp allocated {len(slots)} slots of {sorted({s["n_ctx"] for s in slots})} tokens, but Riko needs ' + f'{self.config.parallel_slots} slots of {required} (the largest of {", ".join(budgets)}).') self._initialize_probe(model) except BaseException: self.client.close() @@ -266,6 +322,13 @@ def _start(self): def _inference_client(self): return NativeClient(self.native, self.config.request_timeout_seconds) + def _flash_attention_identity(self): + # With auto, record what llama.cpp chose (the bridge reports it), so a config that never set flash_attn + # and resolves to off keeps its earlier probe data, and captures with and without it never mix. + notes, requested = getattr(self.native, 'notes', ''), super()._flash_attention_identity() + if requested != 'auto' or 'Flash Attention' not in notes: return requested + return 'Flash Attention enabled' in notes + def _probe_runtime_fingerprint(self): path = Path(self.config.native_library) files = sorted({path, *path.parent.glob('*.dll'), *path.parent.glob('*.so*'), *path.parent.glob('*.dylib')}) diff --git a/Code/process/app_core/inference/llama_runtime.py b/Code/process/app_core/inference/llama_runtime.py index 3a791f2..da98f27 100644 --- a/Code/process/app_core/inference/llama_runtime.py +++ b/Code/process/app_core/inference/llama_runtime.py @@ -10,6 +10,23 @@ 'offload_kqv', 'use_mmap', 'use_mlock', 'main_gpu', 'split_mode', 'tensor_split', 'chat_format', 'cache_size_mb', 'verbose', 'parallel_slots', 'startup_timeout_seconds', 'warmup', 'kv_unified', 'kv_pool_auto', 'kv_pool_tokens', 'pause_background_on_live') +FLASH_ATTENTION = ('auto', 'on', 'off') + + +def flash_attention(value): + """runtime.flash_attn as llama.cpp's --flash-attn value. A YAML boolean keeps its old meaning; PyYAML also + reads the bare on/off that ruamel writes back as booleans.""" + if type(value) is bool: return 'on' if value else 'off' + if value in FLASH_ATTENTION: return value + raise ValueError('runtime.flash_attn must be auto, on or off') + + +def check_native_build(config): + """Settings the pinned llama.cpp cannot load. Checked when the provider starts, not when the YAML loads, + so desktop.setup_on_startup_error keeps Settings open to repair them.""" + if config.split_mode == 'row': + raise ValueError("runtime.split_mode 'row' is not available: llama.cpp removed it from CUDA and ROCm, and Metal " + "and Vulkan never had it. Choose layer (or none for one GPU) in Settings.") def configure_runtime(config, raw): @@ -65,19 +82,21 @@ def validate_runtime(config): value = getattr(config, key) minimum = 1 if key in {'n_batch', 'n_ubatch'} else 0 if type(value) is not int or value < minimum: raise ValueError(f'runtime.{key} must be an integer >= {minimum}') - if type(config.n_gpu_layers) is not int or config.n_gpu_layers < -1: - raise ValueError('runtime.n_gpu_layers must be -1 or a nonnegative integer') + if type(config.n_gpu_layers) is not int or config.n_gpu_layers < -2: + raise ValueError('runtime.n_gpu_layers must be -1 (fit to free GPU memory), -2 (all layers) or a layer count >= 0') if config.n_ubatch > config.n_batch: raise ValueError('runtime.n_ubatch must not exceed n_batch') for key in ('n_threads', 'n_threads_batch'): value = getattr(config, key) if value is not None and (type(value) is not int or value < 1): raise ValueError(f'runtime.{key} must be null or a positive integer') - for key in ('flash_attn', 'offload_kqv', 'use_mmap', 'use_mlock', 'hf_local_files_only', 'verbose'): + for key in ('offload_kqv', 'use_mmap', 'use_mlock', 'hf_local_files_only', 'verbose'): if type(getattr(config, key)) is not bool: raise ValueError(f'runtime.{key} must be boolean') + config.flash_attn = flash_attention(config.flash_attn) for key in ('type_k', 'type_v'): if getattr(config, key) not in KV_TYPES: raise ValueError(f'runtime.{key} must be one of {sorted(KV_TYPES)}') - if config.type_v not in {'f16', 'f32', 'bf16'} and not config.flash_attn: - raise ValueError('Quantized runtime.type_v requires flash_attn: true') - if config.split_mode not in {'none', 'layer', 'row'}: raise ValueError('runtime.split_mode must be none, layer or row') + if config.type_v not in {'f16', 'f32', 'bf16'} and config.flash_attn == 'off': + raise ValueError('Quantized runtime.type_v requires flash_attn auto or on') + # 'row' still loads from YAML so Settings can open and fix it; check_native_build rejects it at startup. + if config.split_mode not in {'none', 'layer', 'row'}: raise ValueError('runtime.split_mode must be layer or none') if config.tensor_split is not None: values = config.tensor_split if not isinstance(values, list) or not values or any(type(v) not in (int, float) or not math.isfinite(v) or v < 0 for v in values) or sum(values) <= 0: diff --git a/Code/process/app_core/integrations/discord/api.py b/Code/process/app_core/integrations/discord/api.py index 0f12cab..7152d00 100644 --- a/Code/process/app_core/integrations/discord/api.py +++ b/Code/process/app_core/integrations/discord/api.py @@ -14,13 +14,10 @@ def transcribe_pcm(session, pcm): import numpy as np - from faster_whisper import WhisperModel + from ...audio.asr import create_whisper with session.asr_lock: model = getattr(session, 'warmed_asr', None) or getattr(getattr(session, 'voice', None), 'model', None) - if model is None: - config = session.config.raw.get('voice', {}) - model = WhisperModel(config.get('asr_model', 'distil-small.en'), - device=config.get('asr_device', 'cuda'), compute_type=config.get('asr_compute_type', 'int8_float16')) + if model is None: model = create_whisper(session.config.raw.get('voice', {})) session.warmed_asr = model samples = np.frombuffer(pcm, dtype=' 0) def _rebuild_index(self): with self.lock: @@ -406,21 +423,17 @@ def _rebuild_index(self): self.index_dirty = False return try: - import faiss + # A plain float32 matrix, rebuilt from record text on every start and never written to disk. + # Not FAISS: on macOS its bundled libomp crashes the process once torch's libomp is also running. with self.embed_lock: vectors = self._embed([r.text for r in records]) - index = faiss.IndexFlatIP(vectors.shape[1]) - index.add(vectors) + vectors.setflags(write=False) mapping = [(r.id, r.revision, r.text) for r in records] with self.lock: # Publish only if the source snapshot is still current. Query fallbacks # cover records added or corrected while vectors were being computed. if self.closed: return if mapping != [(r.id, r.revision, r.text) for r in self.records if r.active]: return - self.config.index_file.parent.mkdir(parents=True, exist_ok=True) - temporary = str(self.config.index_file) + '.tmp' - faiss.write_index(index, temporary) - os.replace(temporary, self.config.index_file) - self.index_snapshot = (index, mapping) + self.index_snapshot = (vectors, mapping) self.index_dirty = False except Exception as exc: with self.lock: @@ -440,11 +453,10 @@ def retrieve(self, query: str, *, exclude_ids=None, return_records=False): if snapshot and self.embedder is not None and self.embed_lock.acquire(blocking=False): try: vector = self._embed([query]) - scores, positions = snapshot[0].search(vector, min(len(snapshot[1]), self.config.max_results * 3)) - for score, position in zip(scores[0], positions[0]): - if position >= 0: - record_id, revision, _ = snapshot[1][position] - semantic[(record_id, revision)] = float(score) + scores, positions = top_inner_products(snapshot[0], vector[0], min(len(snapshot[1]), self.config.max_results * 3)) + for score, position in zip(scores, positions): + record_id, revision, _ = snapshot[1][position] + semantic[(record_id, revision)] = float(score) except Exception as exc: logger.warning('Semantic query failed; using lexical recall: %s', exc) finally: self.embed_lock.release() words = set(re.findall(r'\w+', query.casefold())) diff --git a/Code/process/app_core/resources/vram_estimate.py b/Code/process/app_core/resources/vram_estimate.py index 1b8f940..9adf858 100644 --- a/Code/process/app_core/resources/vram_estimate.py +++ b/Code/process/app_core/resources/vram_estimate.py @@ -140,7 +140,7 @@ def add(key, label, low, high, basis): fraction = min(1, runtime.n_gpu_layers / max(1, layers)) if layers and runtime.n_gpu_layers >= 0 else 1 weight = meta.get('weight_bytes') add('llm_weights', 'Shared LLM weights (one copy)', weight / MIB * fraction if weight else None, - weight / MIB * fraction * 1.1 if weight else None, meta.get('source', 'Unknown model metadata') + '; partial offload approximated by layer fraction') + weight / MIB * fraction * 1.1 if weight else None, meta.get('source', 'Unknown model metadata') + '; partial offload approximated by layer fraction' + ('; -1 assumes the automatic fit offloads every layer' if runtime.n_gpu_layers == -1 else '')) if all(type(v) in (int, float) and v > 0 for v in (layers, embedding, heads, kv_heads)): key_dim = meta.get('key_length') or embedding / heads value_dim = meta.get('value_length') or embedding / heads @@ -157,19 +157,20 @@ def add(key, label, low, high, basis): add('kv', 'KV pool — all slots occupied', pool * kv_per_token, pool * kv_per_token * 1.15, 'Attention dimensions × pooled tokens × selected K/V tensor formats; 15% padding allowance') else: add('kv', 'KV cache (CPU)', 0, 0, 'GPU KV offload disabled') batch = (max(128, runtime.n_ubatch) * embedding * max(4, heads) * 4 + runtime.n_batch * embedding * 8) / MIB - add('llm_compute', 'LLM compute / CUDA graphs', max(128, batch), max(384, batch * (1.25 if runtime.flash_attn else 2)), 'Heuristic workspace affected by batch, microbatch and flash attention') + add('llm_compute', 'LLM compute / CUDA graphs', max(128, batch), max(384, batch * (1.25 if runtime.flash_attn in ('on', True) else 2)), 'Heuristic workspace affected by batch, microbatch and flash attention') else: add('kv', 'KV pool — all slots occupied', None, None, 'Attention metadata unavailable; cannot compute KV bytes per token') add('llm_compute', 'LLM compute workspace', 128, 768, 'Heuristic; missing architecture details') else: add('llm_weights', 'LLM', 0, 0, 'CPU-only owned model' if managed else 'External provider excluded (any local GPU use appears in Other)') voice = config.raw.get('voice', {}) - device = voice.get('asr_device', 'cuda') - model = str(voice.get('asr_model', 'distil-small.en')).lower() + from ..audio.asr import MODEL, resolve + device, precision, note = resolve(voice) + model = str(voice.get('asr_model', MODEL)).lower() base = next((size for name, size in [('large', 3100), ('medium', 1600), ('small', 600), ('base', 200), ('tiny', 100)] if name in model), None) - if device == 'cpu': add('asr', 'Whisper / faster-whisper ASR', 0, 0, 'CPU configured') + if note: warnings.append(f'Speech recognition runs as {device}/{precision} here: {note}.') + if device == 'cpu': add('asr', 'Whisper / faster-whisper ASR', 0, 0, f'CPU ({precision})') elif base: - precision = voice.get('asr_compute_type', 'int8_float16') factor = .65 if precision.startswith('int8') else 2 if precision == 'float32' else 1 add('asr', 'Whisper / faster-whisper ASR', base * factor + 128, base * factor * 1.4 + 384, f'{model}, {precision}, {device}; heuristic resident weights + decoder workspace, included even if microphone is idle') @@ -206,6 +207,9 @@ def add(key, label, low, high, basis): if gpu and gpu.get('other_mib') is not None and not unknown and not runtime.tensor_split: projection = {'low_mib': low + gpu['other_mib'], 'high_mib': high + gpu['other_mib'], 'total_mib': gpu['total_mib'], 'fits_upper_estimate': high + gpu['other_mib'] <= gpu['total_mib']} + if managed and runtime.n_gpu_layers == -1 and projection and projection['high_mib'] + 1024 > projection['total_mib']: + # llama.cpp's --fit offloads every layer only while 1 GiB of device memory stays free at load time. + warnings.append('With GPU layer offload -1, llama.cpp keeps 1 GiB of GPU memory free and would probably run some layers on the CPU (slower) at these settings; -2 requires every layer on the GPU.') return {'components': components, 'low_mib': low, 'high_mib': high, 'complete': not unknown, 'unknown_components': unknown, 'confidence': 'low', 'confidence_basis': 'Model/KV metadata is used where available; compute, recurrent state, ASR and graphics reserves remain heuristic, not a native backend dry-run allocation report.', diff --git a/Code/process/app_core/runtime/lifecycle.py b/Code/process/app_core/runtime/lifecycle.py index 6e662da..4459226 100644 --- a/Code/process/app_core/runtime/lifecycle.py +++ b/Code/process/app_core/runtime/lifecycle.py @@ -4,13 +4,19 @@ logger = logging.getLogger(__name__) -def close_bounded(resource, timeout=1): - close = getattr(resource, 'close', None) - if not close: return +def run_bounded(call, timeout, label): + """Run call on a daemon thread and wait at most timeout seconds; a hung call is abandoned, never joined.""" done = threading.Event() def run(): - try: close() - except Exception: logger.exception('Resource cleanup failed: %s', type(resource).__name__) + try: call() + except Exception: logger.exception('Resource cleanup failed: %s', label) finally: done.set() threading.Thread(target=run, daemon=True, name='resource-cleanup').start() - if not done.wait(timeout): logger.warning('Cleanup deadline exceeded: %s', type(resource).__name__) + finished = done.wait(timeout) + if not finished: logger.warning('Cleanup deadline exceeded: %s', label) + return finished + + +def close_bounded(resource, timeout=1): + close = getattr(resource, 'close', None) + return run_bounded(close, timeout, type(resource).__name__) if close else True diff --git a/Code/process/app_core/runtime/torch_device.py b/Code/process/app_core/runtime/torch_device.py new file mode 100644 index 0000000..20bd157 --- /dev/null +++ b/Code/process/app_core/runtime/torch_device.py @@ -0,0 +1,44 @@ +"""Torch device names for in-process models: auto, cpu, cuda[:n] (ROCm torch reports HIP as cuda) and mps. + +torch is imported only to probe `auto` or to guard a load, so validating configuration stays cheap. +""" +from contextlib import contextmanager +import re +import threading + +_LOAD_LOCK = threading.Lock() +_THREADS = [] + + +def validate(name): + value = str(name).strip().lower() + if value in {'auto', 'cpu', 'cuda', 'mps'} or re.fullmatch(r'cuda:\d+', value): return value + raise ValueError(f'Unsupported torch device {name!r}: use auto, cpu, cuda, cuda: or mps') + + +def resolve(name): + """auto prefers CUDA (also ROCm/HIP), then Apple MPS, then CPU; explicit names pass through.""" + value = validate(name) + if value != 'auto': return value + import torch + if torch.cuda.is_available(): return 'cuda' + mps = getattr(torch.backends, 'mps', None) + return 'mps' if mps is not None and mps.is_available() else 'cpu' + + +@contextmanager +def preserve_torch_globals(): + """Undo process-wide torch settings a model load changes (Julia's configure() sets threads to + JULIA_CPU_THREADS or 4 and matmul precision to 'high'), so its default cap of 4 no longer slows every + other torch user; an explicit JULIA_CPU_THREADS still applies. Loads are serialised so concurrent + warmup loads cannot save each other's capped thread count.""" + import os + import torch + with _LOAD_LOCK: + if not _THREADS: _THREADS.append(torch.get_num_threads()) + precision = torch.get_float32_matmul_precision() + try: yield + finally: + requested = os.environ.get('JULIA_CPU_THREADS', '') + torch.set_num_threads(int(requested) if requested.isdigit() and int(requested) > 0 else _THREADS[0]) + torch.set_float32_matmul_precision(precision) diff --git a/Code/process/app_core/runtime/warmup.py b/Code/process/app_core/runtime/warmup.py index 6014ab3..6235382 100644 --- a/Code/process/app_core/runtime/warmup.py +++ b/Code/process/app_core/runtime/warmup.py @@ -37,10 +37,8 @@ def warm_session(session): session.asr_lock = threading.Lock() def asr(): import numpy as np - from faster_whisper import WhisperModel - settings = session.config.raw.get('voice', {}) - model = WhisperModel(settings.get('asr_model', 'distil-small.en'), device=settings.get('asr_device', 'cuda'), - compute_type=settings.get('asr_compute_type', 'int8_float16')) + from ..audio.asr import create_whisper + model = create_whisper(session.config.raw.get('voice', {})) segments, _ = model.transcribe(np.zeros(16000, dtype='float32'), beam_size=1, vad_filter=False) list(segments) if not session._closed: session.warmed_asr = model diff --git a/Code/process/app_core/tools/registry.py b/Code/process/app_core/tools/registry.py index 819397f..12a7f2f 100644 --- a/Code/process/app_core/tools/registry.py +++ b/Code/process/app_core/tools/registry.py @@ -5,6 +5,7 @@ import json import logging import os +import shutil import subprocess import threading import urllib.request @@ -65,13 +66,24 @@ class RegisteredTool: prepare: Any = None +def resolve_command(command, env): + """Find an MCP server command on the PATH its process will get. Windows CreateProcess applies neither PATHEXT nor + the child's PATH, so npx/uvx (.cmd shims) fail by bare name; a resolved .cmd/.bat path runs through cmd.exe.""" + if os.path.dirname(command): return command # an explicit path runs as written (CreateProcess still adds .exe) + search = os.pathsep.join(os.get_exec_path(env)) + found = shutil.which(command, path=search) + if not found: raise FileNotFoundError(f'MCP server command {command!r} was not found on PATH: {search}') + return found + + class StdioMCPClient: """Minimal MCP JSON-RPC stdio client with a persistent server process.""" def __init__(self, command: str, args: list[str] | None = None, env: dict[str, str] | None = None): self.command, self.args, self.env = command, args, env self._restart_lock = threading.Lock() - self.process = subprocess.Popen([command, *(args or [])], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, text=True, encoding='utf-8', bufsize=1, env={**os.environ, **(env or {})}) + environment = {**os.environ, **(env or {})} + self.process = subprocess.Popen([resolve_command(command, environment), *(args or [])], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, encoding='utf-8', bufsize=1, env=environment) self._counter = 0 self._lock = threading.Lock() self._responses = queue.Queue(maxsize=256) @@ -319,6 +331,8 @@ def from_config(cls, config): except Exception as exc: if client: client.close() logger.warning('Could not load MCP server %s: %s', name, exc) + from ..desktop.state import get_desktop_state + get_desktop_state().notify('tools', f'MCP server {name} was not loaded: {exc}', 'error') return registry except BaseException: registry.close() diff --git a/Code/run_server.py b/Code/run_server.py index 3305ddd..5bd2209 100644 --- a/Code/run_server.py +++ b/Code/run_server.py @@ -1,5 +1,6 @@ """Backend entry point: standalone in development, Electron-owned in releases.""" from pathlib import Path +import asyncio import os import logging @@ -8,6 +9,18 @@ from process.app_core.configuration.config import load_config +class Server(uvicorn.Server): + """uvicorn, except that the active turn ends before requests drain: a reply still generating would hold its + request for the whole drain, and Electron allows the entire shutdown 15 s (electron/release.cjs).""" + def __init__(self, config, stop_turn): + super().__init__(config) + self.stop_turn = stop_turn + + async def shutdown(self, sockets=None): + await asyncio.to_thread(self.stop_turn) + await super().shutdown(sockets=sockets) + + def main(): import sys if '--release-check' in sys.argv: @@ -55,16 +68,22 @@ def main(): listener.listen(2048) desktop_server.api_secrets() if os.environ.get('RIKO_MANAGED') == '1': print('RIKO_BACKEND_LISTENING', flush=True) # see electron/release.cjs - server = uvicorn.Server(uvicorn.Config(desktop_server.app, host="127.0.0.1", port=8765, - log_level="info", timeout_graceful_shutdown=10)) + server = Server(uvicorn.Config(desktop_server.app, host="127.0.0.1", port=8765, + log_level="info", timeout_graceful_shutdown=2), desktop_server.stop_turn) if os.environ.get('RIKO_MANAGED') == '1': + import faulthandler import threading def managed_shutdown(): for line in sys.stdin: if line.strip() == 'shutdown': break + # Electron kills us 15 s after asking (electron/release.cjs). If teardown hangs, write every thread's + # stack to the launch log and exit at 14 s instead of outliving the app with :8765 and the model. + try: faulthandler.dump_traceback_later(14, exit=True) + except Exception: pass # no usable stderr server.should_exit = True threading.Thread(target=managed_shutdown, name='release-shutdown', daemon=True).start() - server.run(sockets=[listener]) + try: server.run(sockets=[listener]) + except KeyboardInterrupt: pass # uvicorn re-raises Ctrl+C after its graceful shutdown; uvicorn.run() swallows it too if __name__ == "__main__": diff --git a/README.md b/README.md index 794ecbe..a3c3f38 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ Install Python runtime dependencies in your virtual environment: .venv\Scripts\python.exe -m pip install --no-deps EfficientWord-Net ``` -`install_reqs.sh` provides the shell-based alternative. Python package metadata +`install_reqs.sh` (macOS, Linux or Git Bash) installs the same into the active environment, or `PYTHON=...`, +using uv when present. It picks the PyTorch wheels for the machine: PyPI (Metal/MPS) on macOS, CUDA 13 (driver 580+) +or 12.6 when `nvidia-smi` sees a GPU, ROCm 7.2 when `rocminfo` exists on Linux, CPU otherwise; set +`RIKO_TORCH_INDEX` (`default`, `cpu`, `cu126`, `cu130`, `rocm7.2`, ...) to choose. Python package metadata requires 3.11 or newer; model dependencies and CUDA wheels must support your chosen Python/GPU combination. The current local environment uses Python 3.14. Installing the requirements alone does not guarantee GPU support. Faster-Whisper GPU use @@ -53,9 +56,10 @@ npm run build npm run start ``` -Electron does not start or stop Python. Quit Python with Ctrl+C; the default -Electron quit shortcut is Ctrl+Shift+Q. Default controls/whiteboard shortcuts are -Ctrl+Shift+Space and Ctrl+Shift+W; supported overrides are in desktop settings. +Electron does not start or stop Python. Quit Python with Ctrl+C. The only default global shortcut is +Ctrl+Shift+Space (Cmd+Shift+Space on macOS), which opens the controls; Quit, Settings and the whiteboard are in +the tray menu. `desktop.shortcuts` binds `popup`, `settings`, `whiteboard`, `quit`, `mic`, `audio` or `sleep` to an +Electron accelerator, and `null` or `''` unbinds one. Electron reads it at launch. For frontend development, run `npm run dev` in `electron/`, then launch Electron from a separate terminal with `$env:RIKO_DEV="1"` and `npm run start`. diff --git a/electron/main.cjs b/electron/main.cjs index 205ba9e..702f3c1 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -9,6 +9,7 @@ let backendProcess,sovitsProcess,setupWindow,shutdownRequested=false; if(app.isPackaged&&!app.requestSingleInstanceLock())app.quit(); app.on('second-instance',()=>{if(setupWindow&&!setupWindow.isDestroyed())setupWindow.focus();else if(control&&!control.isDestroyed())showControls();}); const release=require('./release.cjs'); +const {shortcutBindings}=require('./shortcuts.cjs'); function setupSender(event){if(!setupWindow||setupWindow.isDestroyed()||event.sender.id!==setupWindow.webContents.id)throw new Error('Setup window required');} ipcMain.handle('setup-hardware',async event=>{setupSender(event);const hw=await release.hardware();try{hw.gpus=(await app.getGPUInfo('basic')).gpuDevice?.map(device=>device.deviceString||`GPU vendor ${device.vendorId}, device ${device.deviceId}`)||[];hw.vulkan=hw.gpus.join('\n')+'\n'+hw.vulkan;}catch{}return hw;}); ipcMain.handle('setup-directory',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openDirectory','createDirectory']});return result.canceled?null:result.filePaths[0];}); @@ -220,14 +221,11 @@ app.whenReady().then(async () => { screen.on(name, () => {displaySignature = ''; publishDisplays();}); } if (debug) control.show(); - const shortcuts=config.desktop?.shortcuts||{}; const actions={popup:()=>showControls(),quit:()=>app.quit(),whiteboard:()=>patchBoard({visible:!whiteboard.isVisible()}), settings:()=>showControls('settings'), mic:()=>backendFetch('/api/mic/toggle',{method:'POST'}).catch(()=>{}),audio:()=>backendFetch('/api/audio/toggle',{method:'POST'}).catch(()=>{}),sleep:()=>backendFetch('/api/sleep/toggle',{method:'POST'}).catch(()=>{})}; - const defaults={popup:'CommandOrControl+Shift+Space',quit:'CommandOrControl+Shift+Q',whiteboard:'CommandOrControl+Shift+W',settings:'CommandOrControl+Shift+,'}; - for(const [name,action] of Object.entries(actions)){ - const accelerator=shortcuts[name]??defaults[name]; - if(accelerator){try{if(!globalShortcut.register(accelerator,action))console.warn('Shortcut unavailable:',name,accelerator);}catch(error){console.warn('Invalid shortcut:',name,error.message);}} + for(const [name,accelerator] of shortcutBindings(config.desktop?.shortcuts,Object.keys(actions))){ + try{if(!globalShortcut.register(accelerator,actions[name]))console.warn('Shortcut unavailable:',name,accelerator);}catch(error){console.warn('Invalid shortcut:',name,error.message);} } } catch (error) { dialog.showErrorBox('Desktop startup failed', error.stack || error.message); diff --git a/electron/release.cjs b/electron/release.cjs index 3353b1b..53aa55a 100644 --- a/electron/release.cjs +++ b/electron/release.cjs @@ -21,10 +21,10 @@ function configuration(input,resources){ if(!Number.isInteger(threads)||threads<1||threads>1024)throw new Error('Invalid CPU thread count'); if(!input.modelPath&&(!input.repo||!input.filename||!input.filename.endsWith('.gguf')||input.filename.includes('..')||input.filename.startsWith('/')))throw new Error('Choose a local GGUF or exact Hugging Face repository/file'); if(input.modelPath&&(!path.isAbsolute(input.modelPath)||!input.modelPath.toLowerCase().endsWith('.gguf')||!fs.existsSync(input.modelPath)))throw new Error('Local GGUF does not exist'); - return {runtime:{provider:'llama_cpp',native_library:'bundled:'+input.backend,model_path:input.modelPath||null,hf_repo_id:input.repo||null,hf_filename:input.filename||null,hf_revision:input.revision||'main',n_ctx:context,max_output_tokens:output,n_threads:threads,n_gpu_layers:input.cpuOnly?0:-1,parallel_slots:2,flash_attn:false,type_k:'f16',type_v:'f16',warmup:false}, + return {runtime:{provider:'llama_cpp',native_library:'bundled:'+input.backend,model_path:input.modelPath||null,hf_repo_id:input.repo||null,hf_filename:input.filename||null,hf_revision:input.revision||'main',n_ctx:context,max_output_tokens:output,n_threads:threads,n_gpu_layers:input.cpuOnly?0:-1,parallel_slots:2,flash_attn:'auto',type_k:'f16',type_v:'f16',warmup:false}, presets:{default:{name:input.name||'Riko',system_prompt:input.prompt||'You are a helpful local companion.'}}, memory:{context_window_tokens:context,default_memories:String(input.memories||'').split('\n').filter(t=>t.trim()).map(text=>({text,memory_type:'factual',importance:.8})),embeddings_enabled:!!input.embeddings,system1_enabled:!!input.julia,reflection_enabled:!!input.reflection}, - emotion:{enabled:!!input.julia,device:'cpu',probe:{enabled:false}},voice:{asr_device:'cpu'},tools:{require_approval:true},initiative:{enabled:false},desktop:{setup_on_startup_error:true}, + emotion:{enabled:!!input.julia,device:'cpu',probe:{enabled:false}},voice:{asr_device:'cpu',asr_compute_type:'int8'},tools:{require_approval:true},initiative:{enabled:false},desktop:{setup_on_startup_error:true}, sovits_ping_config:{auto_start:!!input.sovitsAuto,executable:input.sovitsExecutable||null,arguments:[],url:input.sovitsUrl||'http://127.0.0.1:9880/tts',ref_audio_path:input.referenceAudio||'',prompt_text:input.referenceText||'',text_lang:'en',prompt_lang:'en',sample_rate:32000}}; } function saveSetup(directory,input,resources){ @@ -49,13 +49,17 @@ function watchListening(onListening){ if(tail.split(/\r?\n/).slice(0,-1).includes(LISTENING)){tail=null;onListening();} }; } +// Finder and the Dock start apps with launchd's PATH (/usr/bin:/bin:/usr/sbin:/sbin), which hides Homebrew's node, +// npx, uvx and ffmpeg from MCP servers and the Discord worker; put its prefixes first, as a Terminal does. +const HOMEBREW_PATHS=['/opt/homebrew/bin','/opt/homebrew/sbin','/usr/local/bin']; +function finderPath(current){const parts=String(current||'/usr/bin:/bin:/usr/sbin:/sbin').split(':').filter(Boolean);return [...HOMEBREW_PATHS.filter(dir=>!parts.includes(dir)),...parts].join(':');} function startBackend(directory,resources,secrets={},onListening=()=>{}){ const executable=path.join(resources,'backend',process.platform==='win32'?'riko-backend.exe':'riko-backend'); const logPath=path.join(directory,'logs','backend-launch.log'); const log=fs.openSync(logPath,'a'); // This start's API token and confirmation key go to the backend only; it removes them from its own environment. const own=secrets.api_token&&secrets.confirm_key?{RIKO_API_TOKEN:secrets.api_token,RIKO_CONFIRM_KEY:secrets.confirm_key}:{}; - const env={...process.env,...own,RIKO_MANAGED:'1',RIKO_DATA_DIR:directory,RIKO_CONFIG:path.join(directory,'character_config.yaml'),HF_HOME:path.join(directory,'models','huggingface'),TORCH_HOME:path.join(directory,'models','torch'),XDG_CACHE_HOME:path.join(directory,'models','cache'),RIKO_BUNDLE_ROOT:resources}; + const env={...process.env,...own,...(process.platform==='darwin'?{PATH:finderPath(process.env.PATH)}:{}),RIKO_MANAGED:'1',RIKO_DATA_DIR:directory,RIKO_CONFIG:path.join(directory,'character_config.yaml'),HF_HOME:path.join(directory,'models','huggingface'),TORCH_HOME:path.join(directory,'models','torch'),XDG_CACHE_HOME:path.join(directory,'models','cache'),RIKO_BUNDLE_ROOT:resources}; let child; try{child=spawn(executable,[],{cwd:directory,env,stdio:['pipe','pipe',log],windowsHide:true});}finally{fs.closeSync(log);} child.stdin.on('error',()=>{}); @@ -71,5 +75,6 @@ function startSovits(settings){ if(!Array.isArray(args)||args.some(a=>typeof a!=='string'))throw new Error('GPT-SoVITS arguments must be a list of strings'); return spawn(executable,args,{cwd:path.dirname(executable),stdio:'ignore',windowsHide:true,shell:false}); } -function stopBackend(child){return new Promise(resolve=>{if(!child||child.exitCode!==null){resolve();return;}const timeout=setTimeout(()=>{child.kill();resolve();},15000);child.once('exit',()=>{clearTimeout(timeout);resolve();});child.stdin.end('shutdown\n');});} -module.exports={hardware,configuration,saveSetup,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,LISTENING}; +// Ask first; the backend gives itself 14 s. uvicorn ignores SIGTERM while already shutting down, so escalate to SIGKILL. +function stopBackend(child,grace=15000,force=3000){return new Promise(resolve=>{if(!child||child.exitCode!==null||child.signalCode!==null){resolve();return;}let timer=setTimeout(()=>{child.kill();timer=setTimeout(()=>{child.kill('SIGKILL');resolve();},force);},grace);child.once('exit',()=>{clearTimeout(timer);resolve();});child.stdin.end('shutdown\n');});} +module.exports={hardware,configuration,saveSetup,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,finderPath,LISTENING}; diff --git a/electron/shortcuts.cjs b/electron/shortcuts.cjs new file mode 100644 index 0000000..c671237 --- /dev/null +++ b/electron/shortcuts.cjs @@ -0,0 +1,10 @@ +// A global shortcut takes its chord away from every other application, so only the summon shortcut is bound by +// default: Cmd/Ctrl+Shift+Q is macOS Log Out and Chrome's quit, and Cmd/Ctrl+Shift+W closes windows in browsers, +// Finder and editors. Quit, Settings and the whiteboard stay in the tray menu. desktop.shortcuts. binds an +// action; null, '' or false unbinds it, including the default. +const DEFAULT_SHORTCUTS=Object.freeze({popup:'CommandOrControl+Shift+Space'}); +function shortcutBindings(configured,names){ + const shortcuts=configured&&typeof configured==='object'&&!Array.isArray(configured)?configured:{}; + return names.flatMap(name=>{const value=Object.hasOwn(shortcuts,name)?shortcuts[name]:DEFAULT_SHORTCUTS[name];return typeof value==='string'&&value.trim()?[[name,value.trim()]]:[];}); +} +module.exports={DEFAULT_SHORTCUTS,shortcutBindings}; diff --git a/electron/src/discord_notifications.jsx b/electron/src/discord_notifications.jsx index 2cb752b..5b6830e 100644 --- a/electron/src/discord_notifications.jsx +++ b/electron/src/discord_notifications.jsx @@ -2,6 +2,6 @@ import React,{useState} from 'react'; import useRuntime from './use_runtime.jsx'; export default function DiscordNotifications(){ const state=useRuntime(),[dismissed,setDismissed]=useState([]); - const items=(state.notifications||[]).filter(n=>n.source==='discord'&&!dismissed.includes(n.id)).slice(0,3); - return
{items.map(n=>
Discord{n.text}
)}
; + const items=(state.notifications||[]).filter(n=>['discord','tools'].includes(n.source)&&!dismissed.includes(n.id)).slice(0,3); + return
{items.map(n=>
{n.source==='tools'?'Tools':'Discord'}{n.text}
)}
; } diff --git a/electron/src/plain_language.mjs b/electron/src/plain_language.mjs index eeb70f0..47be29d 100644 --- a/electron/src/plain_language.mjs +++ b/electron/src/plain_language.mjs @@ -18,8 +18,8 @@ const help={ 'runtime.hf_revision':'The version of the repository to use.', 'runtime.model_path':'Choose a model file on this computer.', 'runtime.provider':'Choose the service that runs your chat model.', - 'runtime.n_gpu_layers':'Run more model layers on the GPU for speed, or fewer to save GPU memory. Use −1 for all layers.', - 'runtime.flash_attn':'Use a faster, lower-memory attention method when your model and GPU support it.', + 'runtime.n_gpu_layers':'Run more model layers on the GPU for speed, or fewer to save GPU memory. −1 puts as many on the GPU as fit, −2 requires all of them.', + 'runtime.flash_attn':'A faster, lower-memory attention method. Automatic uses it when your model and GPU support it.', 'runtime.type_k':'Choose the precision used to store remembered model context. Lower precision uses less memory.', 'runtime.type_v':'Choose the precision used to store context values. Lower precision uses less memory.', 'runtime.kv_unified':'Share one context cache across chat and background requests.', diff --git a/electron/src/release.test.mjs b/electron/src/release.test.mjs index 1470d20..c8937c5 100644 --- a/electron/src/release.test.mjs +++ b/electron/src/release.test.mjs @@ -4,11 +4,20 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import {createRequire} from 'node:module'; +import {EventEmitter} from 'node:events'; const require=createRequire(import.meta.url); -const {configuration,saveSetup,startSovits,watchListening,LISTENING}=require('../release.cjs'); +const {configuration,saveSetup,startSovits,stopBackend,watchListening,finderPath,LISTENING}=require('../release.cjs'); const YAML=require('yaml'); function fixture(){const root=fs.mkdtempSync(path.join(os.tmpdir(),'riko-release-'));const folder=path.join(root,'native','cuda');fs.mkdirSync(folder,{recursive:true});fs.writeFileSync(path.join(folder,process.platform==='win32'?'riko-native.dll':'libriko-native.so'),'test');return root;} const form={backend:'cuda',repo:'owner/model',filename:'model.gguf',context:8192,output:1024,threads:4,memories:'I like tea.\nMy name is Alex.',sovitsAuto:false}; -test('setup separates persistent data from binaries and refuses overwrite',()=>{const root=fixture();const directory=fs.mkdtempSync(path.join(os.tmpdir(),'riko-data-'));try{saveSetup(directory,form,root);const config=YAML.parse(fs.readFileSync(path.join(directory,'character_config.yaml'),'utf8'));assert.equal(config.runtime.native_library,'bundled:cuda');assert.equal(config.memory.default_memories.length,2);assert.equal(config.tools.require_approval,true);assert.equal(config.emotion.probe.enabled,false);assert.ok(fs.existsSync(path.join(directory,'models')));assert.throws(()=>saveSetup(directory,form,root),/EEXIST/);assert.throws(()=>saveSetup(root,form,root),/outside/);}finally{fs.rmSync(root,{recursive:true,force:true});fs.rmSync(directory,{recursive:true,force:true});}}); +test('setup separates persistent data from binaries and refuses overwrite',()=>{const root=fixture();const directory=fs.mkdtempSync(path.join(os.tmpdir(),'riko-data-'));try{saveSetup(directory,form,root);const config=YAML.parse(fs.readFileSync(path.join(directory,'character_config.yaml'),'utf8'));assert.equal(config.runtime.native_library,'bundled:cuda');assert.equal(config.memory.default_memories.length,2);assert.equal(config.tools.require_approval,true);assert.equal(config.emotion.probe.enabled,false);assert.deepEqual(config.voice,{asr_device:'cpu',asr_compute_type:'int8'});assert.ok(fs.existsSync(path.join(directory,'models')));assert.throws(()=>saveSetup(directory,form,root),/EEXIST/);assert.throws(()=>saveSetup(root,form,root),/outside/);}finally{fs.rmSync(root,{recursive:true,force:true});fs.rmSync(directory,{recursive:true,force:true});}}); test('setup validates budgets, model and explicit speech consent',()=>{const root=fixture();try{assert.throws(()=>configuration({...form,output:8192},root),/budget/);assert.throws(()=>configuration({...form,filename:'../secret.gguf'},root),/GGUF/);assert.throws(()=>configuration({...form,sovitsAuto:true},root),/GPT-SoVITS/);assert.equal(startSovits({auto_start:false}),null);assert.throws(()=>startSovits({auto_start:true,executable:'relative.exe'}),/missing/);}finally{fs.rmSync(root,{recursive:true,force:true});}}); test('main learns the backend holds its port only from a complete marker line',()=>{let calls=0;const watch=watchListening(()=>calls++);watch('Riko AI server: http://127.0.0.1:8765\nRIKO_BACKEND_');assert.equal(calls,0);watch('LISTENING_NOT\n');assert.equal(calls,0);watch(LISTENING+'\r\nINFO later\n');assert.equal(calls,1);watch(LISTENING+'\n');assert.equal(calls,1);const split=watchListening(()=>calls++);for(const part of ['RIKO_BACK','END_LISTEN','ING','\n'])split(Buffer.from(part));assert.equal(calls,2);}); +function fakeChild({exitOnShutdown=false,signalCode=null}={}){const child=Object.assign(new EventEmitter(),{exitCode:null,signalCode,signals:[],written:'',kill(signal='SIGTERM'){child.signals.push(signal);return true;}});child.stdin={end(text){child.written+=text;if(exitOnShutdown)setImmediate(()=>child.emit('exit',0,null));}};return child;} +test('stopBackend asks for shutdown, then escalates to SIGTERM and SIGKILL',async()=>{const hung=fakeChild();await stopBackend(hung,20,20);assert.equal(hung.written,'shutdown\n');assert.deepEqual(hung.signals,['SIGTERM','SIGKILL']);const quick=fakeChild({exitOnShutdown:true});await stopBackend(quick,20,20);assert.deepEqual(quick.signals,[]);const crashed=fakeChild({signalCode:'SIGABRT'}),started=Date.now();await stopBackend(crashed,1000,1000);assert.ok(Date.now()-started<500);assert.equal(crashed.written,'');}); +test('a Finder-launched macOS backend still finds Homebrew tools for MCP servers and FFmpeg',()=>{ + assert.equal(finderPath('/usr/bin:/bin:/usr/sbin:/sbin'),'/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'); + assert.equal(finderPath('/usr/local/bin:/opt/homebrew/bin:/usr/bin'),'/opt/homebrew/sbin:/usr/local/bin:/opt/homebrew/bin:/usr/bin'); + assert.equal(finderPath(undefined),'/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'); + assert.match(fs.readFileSync(new URL('../release.cjs',import.meta.url),'utf8'),/process\.platform==='darwin'\?\{PATH:finderPath\(process\.env\.PATH\)\}/); +}); diff --git a/electron/src/settings_model.mjs b/electron/src/settings_model.mjs index 1adcf3f..4681b73 100644 --- a/electron/src/settings_model.mjs +++ b/electron/src/settings_model.mjs @@ -27,7 +27,7 @@ export function settingsPatch(fields, values, inputs) { if (inputs['runtime.provider'] === 'llama_cpp') { if (!inputs['runtime.model_path'] && (!inputs['runtime.hf_repo_id'] || !inputs['runtime.hf_filename'])) errors['runtime.hf_repo_id'] = 'Choose a local GGUF or a Hugging Face repository and file'; if (Number(inputs['runtime.n_ubatch']) > Number(inputs['runtime.n_batch'])) errors['runtime.n_ubatch'] = 'Physical batch must not exceed logical batch'; - if (!['f16','f32','bf16'].includes(inputs['runtime.type_v']) && !inputs['runtime.flash_attn']) errors['runtime.type_v'] = 'Quantized V requires flash attention'; + if (!['f16','f32','bf16'].includes(inputs['runtime.type_v']) && [false,'off'].includes(inputs['runtime.flash_attn'])) errors['runtime.type_v'] = 'Quantized V requires flash attention auto or on'; } return {changes, errors}; } @@ -43,9 +43,9 @@ export function llamaServerCommand(inputs) { } export const runtimePresets = { - balanced: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 512, 'runtime.n_ubatch': 512, 'runtime.flash_attn': false, 'runtime.type_k': 'f16', 'runtime.type_v': 'f16'}, - compact: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 256, 'runtime.n_ubatch': 128, 'runtime.flash_attn': true, 'runtime.type_k': 'q8_0', 'runtime.type_v': 'q8_0'}, - cpu: {'runtime.n_gpu_layers': 0, 'runtime.n_batch': 256, 'runtime.n_ubatch': 128, 'runtime.flash_attn': false, 'runtime.type_k': 'f16', 'runtime.type_v': 'f16'}, + balanced: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 512, 'runtime.n_ubatch': 512, 'runtime.flash_attn': 'auto', 'runtime.type_k': 'f16', 'runtime.type_v': 'f16'}, + compact: {'runtime.n_gpu_layers': -1, 'runtime.n_batch': 256, 'runtime.n_ubatch': 128, 'runtime.flash_attn': 'auto', 'runtime.type_k': 'q8_0', 'runtime.type_v': 'q8_0'}, + cpu: {'runtime.n_gpu_layers': 0, 'runtime.n_batch': 256, 'runtime.n_ubatch': 128, 'runtime.flash_attn': 'auto', 'runtime.type_k': 'f16', 'runtime.type_v': 'f16'}, }; export function inputValues(snapshot) { @@ -59,7 +59,7 @@ export function sliderSpec(field,value){ const compute=['runtime.n_batch','runtime.n_ubatch','runtime.n_gpu_layers'].includes(field.path); let min=field.min,max=field.max,step=field.integer?1:.01; if(budgets){min=field.min??1;max=Math.min(field.max??65536,65536);step=256;} - else if(compute){min=field.min??(field.path.endsWith('n_gpu_layers')?-1:1);max=field.path.endsWith('n_gpu_layers')?128:4096;step=1;} + else if(compute){min=field.min??(field.path.endsWith('n_gpu_layers')?-2:1);max=field.path.endsWith('n_gpu_layers')?128:4096;step=1;} else if(!Number.isFinite(min)||!Number.isFinite(max)||max-min>1000)return null; else if(!field.integer&&max-min>10)step=.1; return {min:Math.min(min,Number(value)),max:Math.max(max,Number(value)),step}; diff --git a/electron/src/settings_model.test.mjs b/electron/src/settings_model.test.mjs index a62fb7e..319fb7f 100644 --- a/electron/src/settings_model.test.mjs +++ b/electron/src/settings_model.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import {test} from 'node:test'; -import {parseSetting,settingsPatch,inputValues,llamaServerCommand} from './settings_model.mjs'; +import {parseSetting,settingsPatch,inputValues,llamaServerCommand,runtimePresets} from './settings_model.mjs'; test('numeric inputs validate immediately and do not coerce blanks to zero',()=>{ const field={kind:'number',integer:true,min:2,max:4}; @@ -20,6 +20,11 @@ test('native cross-field validation enforces physical batch and V-cache constrai const values={'runtime.provider':'llama_cpp','runtime.model_path':'model.gguf','runtime.n_batch':128,'runtime.n_ubatch':256,'runtime.type_v':'q8_0','runtime.flash_attn':false}; const result=settingsPatch([],{},values); assert.ok(result.errors['runtime.n_ubatch']);assert.ok(result.errors['runtime.type_v']); + for(const flash of ['off',false])assert.ok(settingsPatch([],{},{...values,'runtime.n_ubatch':128,'runtime.flash_attn':flash}).errors['runtime.type_v']); + for(const flash of ['auto','on'])assert.deepEqual(settingsPatch([],{},{...values,'runtime.n_ubatch':128,'runtime.flash_attn':flash}).errors,{}); +}); +test('runtime presets use llama.cpp automatic flash attention',()=>{ + for(const preset of Object.values(runtimePresets))assert.equal(preset['runtime.flash_attn'],'auto'); }); test('form hydration preserves zero, false and JSON arrays',()=>{ const values={zero:0,flag:false,split:[1,1]}; diff --git a/electron/src/settings_page.jsx b/electron/src/settings_page.jsx index fe5cc07..551f040 100644 --- a/electron/src/settings_page.jsx +++ b/electron/src/settings_page.jsx @@ -131,7 +131,7 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) try {const value=await request('/api/settings/path',{method:'POST',body:{value:String(inputs[field.path]||'')}});setPathChecks(old=>({...old,[field.path]:value}));} catch(e){setError(e.message);} } - function preset(name){setInputs(old=>syncPool({...old,...runtimePresets[name]}));setNotice(name==='compact'?'Compact preset requires compatible flash attention and quantized KV support. No hardware capability is inferred.':'Preset applied to your draft. Review and save when ready.');} + function preset(name){setInputs(old=>syncPool({...old,...runtimePresets[name]}));setNotice(name==='compact'?'Compact preset quantizes the KV cache, which turns flash attention on. If your GPU backend lacks flash attention for this model, attention runs on the CPU.':'Preset applied to your draft. Review and save when ready.');} const searchItems=useMemo(()=>settingsIndex(snapshot?.fields||[],groups,catalog),[snapshot?.fields,catalog]); const fields=(snapshot?.fields||[]).filter(field=>field.path===destination?.path||(field.group===group &&!['avatar.model','avatar.format','runtime.kv_pool_auto','runtime.kv_pool_tokens'].includes(field.path) diff --git a/electron/src/shortcuts.test.mjs b/electron/src/shortcuts.test.mjs new file mode 100644 index 0000000..9b3b04b --- /dev/null +++ b/electron/src/shortcuts.test.mjs @@ -0,0 +1,23 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {createRequire} from 'node:module'; +const require=createRequire(import.meta.url); +const {DEFAULT_SHORTCUTS,shortcutBindings}=require('../shortcuts.cjs'); +const names=['popup','quit','whiteboard','settings','mic','audio','sleep']; + +test('only the summon shortcut is global by default; no OS or browser chord is taken',()=>{ + assert.deepEqual(shortcutBindings(undefined,names),[['popup','CommandOrControl+Shift+Space']]); + assert.deepEqual(Object.keys(DEFAULT_SHORTCUTS),['popup']); + for(const chord of Object.values(DEFAULT_SHORTCUTS))assert.doesNotMatch(chord,/Shift\+[QW]$/); +}); +test('configured shortcuts bind actions, and null, empty or false unbinds even the default',()=>{ + assert.deepEqual(shortcutBindings({quit:'CommandOrControl+Shift+Q',mic:' Alt+M ',unknown:'Alt+U'},names),[['popup','CommandOrControl+Shift+Space'],['quit','CommandOrControl+Shift+Q'],['mic','Alt+M']]); + for(const off of [null,'',false,0])assert.deepEqual(shortcutBindings({popup:off},names),[]); + assert.deepEqual(shortcutBindings(['Alt+X'],names),[['popup','CommandOrControl+Shift+Space']]); +}); +test('main registers shortcuts only through shortcutBindings',()=>{ + const host=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8'); + assert.match(host,/shortcutBindings\(config\.desktop\?\.shortcuts,Object\.keys\(actions\)\)/); + assert.doesNotMatch(host,/CommandOrControl\+Shift\+[QW,]/); +}); diff --git a/install_reqs.sh b/install_reqs.sh old mode 100644 new mode 100755 index 07fbef2..677f21f --- a/install_reqs.sh +++ b/install_reqs.sh @@ -1,11 +1,57 @@ -pip install uv -uv pip install torch==2.10.0 torchaudio==2.10.0 --index-url https://download.pytorch.org/whl/cu130 -uv pip install -r requirements-runtime.txt -uv pip install --no-deps EfficientWord-Net -echo "Place your VRM avatar model in electron/public/models/ after installing the Electron dependencies." +#!/usr/bin/env bash +# Installs the Python runtime into the active environment: activate the virtual environment first, or set PYTHON. +# The PyTorch wheels follow the machine: PyPI on macOS (Metal/MPS), CUDA when nvidia-smi sees a GPU, ROCm when +# rocminfo exists on Linux, CPU otherwise. Override with RIKO_TORCH_INDEX=default|cpu|cu126|cu130|rocm7.2|... +set -euo pipefail +cd "$(dirname "$0")" + +# Without an active environment, prefer this repository's .venv over whatever python is first on PATH. +if [ -z "${PYTHON:-}" ]; then + if [ -z "${VIRTUAL_ENV:-}" ] && [ -x .venv/bin/python ]; then PYTHON=.venv/bin/python + elif [ -z "${VIRTUAL_ENV:-}" ] && [ -x .venv/Scripts/python.exe ]; then PYTHON=.venv/Scripts/python.exe + else PYTHON="$(command -v python || command -v python3 || true)"; fi +fi +if [ -z "$PYTHON" ]; then echo "No Python found: activate the virtual environment or set PYTHON." >&2; exit 1; fi +echo "Installing into $PYTHON" +"$PYTHON" -c 'import sys; sys.exit(sys.version_info < (3, 11))' || { echo "$PYTHON is not a working Python 3.11 or newer." >&2; exit 1; } +if command -v uv >/dev/null 2>&1; then pip_install() { uv pip install --python "$PYTHON" "$@"; } +else pip_install() { "$PYTHON" -m pip install "$@"; }; fi -python - </dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + driver="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n 1 || true)" + cap="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -n 1 || true)" + major="${cap%%.*}"; minor="${cap#*.}"; old_gpu=no; blackwell=no + case "$major" in ''|*[!0-9]*) ;; *) + if [ "$major" -lt 7 ] || { [ "$major" -eq 7 ] && [ "$minor" -lt 5 ] 2>/dev/null; }; then old_gpu=yes; fi + if [ "$major" -ge 10 ]; then blackwell=yes; fi ;; + esac + # CUDA 13 wheels need driver 580+ and compute capability 7.5+ (Turing or newer). CUDA 12.6 still runs + # Maxwell to Volta but has no Blackwell kernels, so a Blackwell GPU needs the newer driver. + if [ "$old_gpu" = yes ]; then index=cu126 + elif [ "${driver%%.*}" -ge 580 ] 2>/dev/null; then index=cu130 + elif [ "$blackwell" = yes ]; then + echo "This GPU (compute capability $cap) needs NVIDIA driver 580 or newer for PyTorch: update the driver, or set RIKO_TORCH_INDEX." >&2; exit 1 + else index=cu126; fi + elif [ "$system" = Linux ] && { command -v rocminfo >/dev/null 2>&1 || [ -x /opt/rocm/bin/rocminfo ]; }; then index=rocm7.2 + else index=cpu; fi +fi +case "$index" in + default) pip_install torch ;; + cpu|xpu|cu[0-9]*|rocm[0-9]*) pip_install torch --index-url "https://download.pytorch.org/whl/$index" ;; + *) echo "Unknown RIKO_TORCH_INDEX: $index" >&2; exit 1 ;; +esac +pip_install -r requirements-runtime.txt +# Wake enrollment uses only EfficientWord-Net's ONNX module; PyAudio and its other declared dependencies are unused. +pip_install --no-deps EfficientWord-Net +"$PYTHON" - <<'PY' +import torch +if torch.cuda.is_available(): + try: torch.ones(1, device='cuda').add_(1); print('torch', torch.__version__, 'GPU') + except Exception as exc: print('torch', torch.__version__, 'sees a GPU these wheels cannot run on:', exc) +else: print('torch', torch.__version__, 'MPS' if torch.backends.mps.is_available() else 'CPU only') +PY +echo "Place your VRM avatar model in electron/public/models/ after installing the Electron dependencies." diff --git a/pyproject.toml b/pyproject.toml index 4b98cba..1142537 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,15 @@ dependencies = ["pyyaml>=6.0", "ruamel.yaml>=0.18", "openai>=1.0"] [project.optional-dependencies] llama = ["huggingface_hub", "httpx"] -desktop = ["fastapi>=0.115", "uvicorn>=0.30", "websockets>=12", "pillow>=10"] +desktop = ["fastapi>=0.115", "uvicorn>=0.30", "websockets>=12", "pillow>=10", "httpx"] discord = ["discord.py>=2.7.1,<3", "python-dotenv>=1.0", "aiohttp>=3.9", "pypdf>=4"] -test = ["pytest>=8.0"] +# Exactly requirements-runtime.txt (tests/test_dependency_manifests.py keeps them equal). EfficientWord-Net +# is installed separately with --no-deps; install_reqs.sh picks the PyTorch wheels for the machine. +runtime = ["openai>=1.0", "pyyaml>=6.0", "ruamel.yaml>=0.18", "pypdf>=4", "huggingface_hub", + "sentence-transformers", "opencv-python", "httpx", "numpy", "requests", "sounddevice", "soundfile", "torch", + "faster-whisper", "silero-vad", "scipy", "pillow>=10", "discord.py>=2.7.1,<3", "python-dotenv>=1.0", "aiohttp>=3.9", + "transformers", "fastapi>=0.115", "uvicorn>=0.30", "websockets>=12", "onnxruntime"] +test = ["riko-companion[runtime]", "pytest>=8.0"] [tool.pytest.ini_options] pythonpath = ["Code"] diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 2f0c9e9..f76d024 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -1,11 +1,10 @@ # Complete local runtime: desktop, LM Studio/OpenAI-compatible providers, # Julia 1, memory, voice input/output, built-in tools, and Discord. -openai -pyyaml -ruamel.yaml -pypdf +openai>=1.0 +pyyaml>=6.0 +ruamel.yaml>=0.18 +pypdf>=4 huggingface_hub -faiss-cpu sentence-transformers opencv-python httpx @@ -19,11 +18,12 @@ silero-vad scipy pillow>=10 discord.py>=2.7.1,<3 -python-dotenv +python-dotenv>=1.0 +aiohttp>=3.9 transformers -fastapi -uvicorn -websockets +fastapi>=0.115 +uvicorn>=0.30 +websockets>=12 onnxruntime # Wake enrollment uses EfficientWord-Net's ONNX module only. Install its wheel # with --no-deps (see install_reqs.sh); PyAudio is not used by this application. diff --git a/tests/test_asr.py b/tests/test_asr.py new file mode 100644 index 0000000..2ae638a --- /dev/null +++ b/tests/test_asr.py @@ -0,0 +1,96 @@ +"""One ASR factory: auto/default resolve to what CTranslate2 can run here, and a pair it cannot run falls back once.""" +import logging +from pathlib import Path +import re +import sys +import threading +from types import SimpleNamespace + +import pytest + +from process.app_core.audio import asr + +MAC = {'cpu': {'float32', 'int8', 'int8_float32'}} +RTX = {**MAC, 'cuda': {'float32', 'float16', 'bfloat16', 'int8', 'int8_float32', 'int8_float16', 'int8_bfloat16'}} +PASCAL = {**MAC, 'cuda': {'float32', 'int8', 'int8_float32'}} + + +def machine(monkeypatch, types): + monkeypatch.setattr(asr, 'supported_types', lambda: {device: frozenset(names) for device, names in types.items()}) + monkeypatch.setattr(asr, '_warned', set()) + + +def whisper(monkeypatch): + built = [] + class WhisperModel: + def __init__(self, name, **options): built.append((name, options)) + def transcribe(self, samples, **options): return iter([SimpleNamespace(text=' heard you ')]), None + monkeypatch.setitem(sys.modules, 'faster_whisper', SimpleNamespace(WhisperModel=WhisperModel)) + return built + + +@pytest.mark.parametrize('types, voice, device, precision, note', [ + (MAC, {}, 'cpu', 'int8', ''), + (MAC, {'asr_device': 'cuda', 'asr_compute_type': 'int8_float16'}, 'cpu', 'int8', 'no CUDA device is visible to CTranslate2; cpu does not support int8_float16'), + (MAC, {'asr_device': 'cpu'}, 'cpu', 'int8', ''), + (MAC, {'asr_device': 'cpu', 'asr_compute_type': 'int8_float16'}, 'cpu', 'int8', 'cpu does not support int8_float16'), + (MAC, {'asr_device': 'auto', 'asr_compute_type': 'float16'}, 'cpu', 'int8', 'cpu does not support float16'), + (MAC, {'asr_device': 'cuda', 'asr_compute_type': 'float32'}, 'cpu', 'float32', 'no CUDA device is visible to CTranslate2'), + (MAC, {'asr_device': 'mps'}, 'cpu', 'int8', 'unknown device mps'), + (RTX, {}, 'cuda', 'int8_float16', ''), + (RTX, {'asr_device': 'cuda', 'asr_compute_type': 'int8_float16'}, 'cuda', 'int8_float16', ''), + (RTX, {'asr_device': 'cpu', 'asr_compute_type': 'default'}, 'cpu', 'int8', ''), + (PASCAL, {}, 'cuda', 'int8_float32', ''), + (PASCAL, {'asr_device': 'cuda', 'asr_compute_type': 'int8_float16'}, 'cuda', 'int8_float32', 'cuda does not support int8_float16'), +]) +def test_resolution_keeps_runnable_pairs_and_replaces_the_rest(monkeypatch, types, voice, device, precision, note): + machine(monkeypatch, types) + assert asr.resolve(voice) == (device, precision, note) + + +def test_resolved_default_is_accepted_by_this_ctranslate2_build(): + ctranslate2 = pytest.importorskip('ctranslate2') + asr.supported_types.cache_clear() + try: + device, precision, note = asr.resolve({}) + assert not note and precision in ctranslate2.get_supported_compute_types(device) + assert asr.resolve({'asr_device': 'cpu', 'asr_compute_type': 'int8_float16'})[:2] == ('cpu', 'int8') + finally: asr.supported_types.cache_clear() + + +def test_probe_lists_cuda_only_with_a_visible_device(monkeypatch): + def supported(device): + if device == 'cuda': raise ValueError('This CTranslate2 package was not compiled with CUDA support') + return {'float32', 'int8'} + monkeypatch.setitem(sys.modules, 'ctranslate2', SimpleNamespace(get_cuda_device_count=lambda: 0, get_supported_compute_types=supported)) + asr.supported_types.cache_clear() + try: assert asr.supported_types() == {'cpu': frozenset({'float32', 'int8'})} + finally: asr.supported_types.cache_clear() + + +def test_factory_builds_the_resolved_pair_and_warns_once(monkeypatch, caplog): + machine(monkeypatch, MAC) + built = whisper(monkeypatch) + voice = {'asr_device': 'cuda', 'asr_compute_type': 'int8_float16'} + with caplog.at_level(logging.WARNING, logger=asr.__name__): + asr.create_whisper(voice); asr.create_whisper(voice) + assert built == [('distil-small.en', {'device': 'cpu', 'compute_type': 'int8'})] * 2 + assert len([record for record in caplog.records if 'cannot run here' in record.getMessage()]) == 1 + + +def test_discord_transcription_builds_through_the_factory(monkeypatch): + from process.app_core.integrations.discord.api import transcribe_pcm + machine(monkeypatch, MAC) + built = whisper(monkeypatch) + session = SimpleNamespace(asr_lock=threading.Lock(), warmed_asr=None, voice=None, + config=SimpleNamespace(raw={'voice': {'asr_device': 'cpu', 'asr_compute_type': 'int8_float16'}})) + assert transcribe_pcm(session, bytes(3200)) == 'heard you' + assert built == [('distil-small.en', {'device': 'cpu', 'compute_type': 'int8'})] and session.warmed_asr is not None + + +def test_only_the_factory_constructs_whisper_or_defaults_the_pair(): + root = Path(__file__).resolve().parents[1] / 'Code' + pattern = re.compile(r"WhisperModel\(|asr_device['\"]\s*[,:]\s*['\"]cuda|asr_compute_type['\"]\s*[,:]\s*['\"]int8_float16") + hits = [f'{path.relative_to(root)}:{number}' for path in root.rglob('*.py') if path.relative_to(root).as_posix() != 'process/app_core/audio/asr.py' + for number, line in enumerate(path.read_text(encoding='utf-8').splitlines(), 1) if pattern.search(line)] + assert not hits, 'Build faster-whisper through audio/asr.py:create_whisper:\n' + '\n'.join(hits) diff --git a/tests/test_backend_shutdown.py b/tests/test_backend_shutdown.py new file mode 100644 index 0000000..b7ae735 --- /dev/null +++ b/tests/test_backend_shutdown.py @@ -0,0 +1,36 @@ +import socket +import threading +import time +import urllib.request + +import uvicorn +from fastapi import FastAPI + +import run_server + + +def test_shutdown_stops_the_turn_before_uvicorn_drains_requests(): + """A reply still generating must end at shutdown, not hold its request through uvicorn's drain.""" + cancel, order, result = threading.Event(), [], {} + app = FastAPI() + @app.post('/chat') + def chat(): + order.append('chat') + return {'cancelled': cancel.wait(10)} + listener = socket.socket(); listener.bind(('127.0.0.1', 0)); listener.listen(8) + port = listener.getsockname()[1] + server = run_server.Server(uvicorn.Config(app, log_level='warning', timeout_graceful_shutdown=10), + lambda: (order.append('stop'), cancel.set())) + thread = threading.Thread(target=server.run, kwargs={'sockets': [listener]}, daemon=True) + thread.start() + deadline = time.monotonic() + 10 + while not server.started and time.monotonic() < deadline: time.sleep(.01) + request = urllib.request.Request(f'http://127.0.0.1:{port}/chat', method='POST') + client = threading.Thread(target=lambda: result.update(body=urllib.request.urlopen(request, timeout=10).read())) + client.start() + while 'chat' not in order and time.monotonic() < deadline: time.sleep(.01) + started = time.monotonic() + server.should_exit = True + thread.join(10); client.join(10) + assert not thread.is_alive() and time.monotonic() - started < 3 # not the 10 s drain + assert order == ['chat', 'stop'] and result['body'] == b'{"cancelled":true}' diff --git a/tests/test_dependency_manifests.py b/tests/test_dependency_manifests.py new file mode 100644 index 0000000..89a4b5b --- /dev/null +++ b/tests/test_dependency_manifests.py @@ -0,0 +1,21 @@ +from pathlib import Path +import re +import tomllib + +ROOT = Path(__file__).resolve().parents[1] + + +def requirement(text): + name, spec = re.match(r'\s*([A-Za-z0-9._-]+)\s*(.*)', text).groups() + return re.sub(r'[-_.]+', '-', name).lower(), spec.replace(' ', '') + + +def test_runtime_extra_mirrors_requirements_runtime_and_covers_every_extra(): + project = tomllib.loads((ROOT / 'pyproject.toml').read_text(encoding='utf-8'))['project'] + extras = project['optional-dependencies'] + lines = [line.split('#')[0].strip() for line in (ROOT / 'requirements-runtime.txt').read_text(encoding='utf-8').splitlines()] + runtime = {requirement(line) for line in lines if line} + assert {requirement(item) for item in extras['runtime']} == runtime + for name in ('llama', 'desktop', 'discord'): assert {requirement(item) for item in extras[name]} <= runtime, name + assert {requirement(item) for item in project['dependencies']} <= runtime + assert 'riko-companion[runtime]' in extras['test'] diff --git a/tests/test_desktop_api.py b/tests/test_desktop_api.py index 2c23339..fa89a49 100644 --- a/tests/test_desktop_api.py +++ b/tests/test_desktop_api.py @@ -1,6 +1,8 @@ from types import SimpleNamespace import importlib import os +import threading +import time import pytest from fastapi.testclient import TestClient @@ -491,3 +493,35 @@ def test_secrets_are_fresh_each_start_and_never_inherited_by_children(tmp_path, assert client_token() == token monkeypatch.setenv('RIKO_API_TOKEN', 'c' * 43) assert client_token() == 'c' * 43 + + +def test_stop_turn_cancels_the_live_session_before_the_drain(backend, monkeypatch): + calls = [] + monkeypatch.setattr(backend, 'session', SimpleNamespace(_closed=False, cancel=lambda: calls.append('cancel'))) + backend.stop_turn() + assert calls == ['cancel'] + monkeypatch.setattr(backend, 'session', SimpleNamespace(_closed=True, cancel=lambda: calls.append('closed'))) + backend.stop_turn() + monkeypatch.setattr(backend, 'session', None) # setup mode: nothing to stop + backend.stop_turn() + stuck = threading.Event() + monkeypatch.setattr(backend, 'session', SimpleNamespace(_closed=False, cancel=lambda: stuck.wait(5))) + started = time.monotonic() + try: backend.stop_turn() + finally: stuck.set() + assert calls == ['cancel'] and time.monotonic() - started < 3 # a stuck cancel is abandoned after 1 s + + +def test_lifespan_stops_discord_while_the_session_closes(backend, monkeypatch): + closing, calls = threading.Event(), [] + monkeypatch.setattr(backend, 'create_chat_service', lambda config: SimpleNamespace()) + class Session: + def __init__(self, *args): pass + def runtime_snapshot(self): return {'runtime': {}} + def close(self): closing.set(); calls.append('close') + monkeypatch.setattr(backend, 'SessionManager', Session) + monkeypatch.setattr(backend, 'Initiative', lambda session: None) + monkeypatch.setattr(backend, 'discord_launcher', SimpleNamespace(status=lambda: {'running': False, 'error': ''}, + stop=lambda: calls.append(('discord', closing.wait(5))))) + with client_for(backend): pass + assert ('discord', True) in calls and 'close' in calls # Discord's stop overlapped the session close diff --git a/tests/test_gpu_resources.py b/tests/test_gpu_resources.py index e88bdc8..911ee07 100644 --- a/tests/test_gpu_resources.py +++ b/tests/test_gpu_resources.py @@ -73,7 +73,15 @@ def test_cpu_offload_and_external_provider_exclusions(tmp_path): assert next(row for row in result['components'] if row['id'] == 'tts')['high_mib'] == 0 -def test_asr_model_precision_and_batches_affect_estimate(tmp_path): +def ctranslate2(monkeypatch, cuda): + from process.app_core.audio import asr + types = {'cpu': frozenset({'float32', 'int8', 'int8_float32'})} + if cuda: types['cuda'] = frozenset({'float32', 'float16', 'int8', 'int8_float32', 'int8_float16'}) + monkeypatch.setattr(asr, 'supported_types', lambda: types) + + +def test_asr_model_precision_and_batches_affect_estimate(tmp_path, monkeypatch): + ctranslate2(monkeypatch, cuda=True) c = config(tmp_path) c.raw['voice'] = {'asr_device': 'cuda', 'asr_model': 'small', 'asr_compute_type': 'int8_float16'} compact = estimate(c, gpu(), metadata()) @@ -81,7 +89,18 @@ def test_asr_model_precision_and_batches_affect_estimate(tmp_path): c.raw['voice']['asr_compute_type'] = 'float32' c.runtime.n_batch = c.runtime.n_ubatch = 2048 large = estimate(c, gpu(), metadata()) - assert large['high_mib'] > compact['high_mib'] + asr = lambda result: next(row for row in result['components'] if row['id'] == 'asr')['high_mib'] + assert large['high_mib'] > compact['high_mib'] and asr(large) > asr(compact) > 0 + + +def test_asr_estimate_uses_the_device_it_will_actually_run_on(tmp_path, monkeypatch): + ctranslate2(monkeypatch, cuda=False) + c = config(tmp_path) + c.raw['voice'] = {'asr_device': 'cuda', 'asr_model': 'small', 'asr_compute_type': 'int8_float16'} + result = estimate(c, gpu(), metadata()) + row = lambda key: next(row for row in result['components'] if row['id'] == key) + assert row('asr')['high_mib'] == 0 and row('cuda')['high_mib'] == 0 and row('asr')['basis'] == 'CPU (int8)' + assert any('runs as cpu/int8' in warning for warning in result['warnings']) def test_unknown_metadata_suppresses_false_total_and_suggestion(tmp_path): diff --git a/tests/test_install_script.py b/tests/test_install_script.py new file mode 100644 index 0000000..71f8a15 --- /dev/null +++ b/tests/test_install_script.py @@ -0,0 +1,54 @@ +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / 'install_reqs.sh' +pytestmark = pytest.mark.skipif(os.name == 'nt' or not shutil.which('bash'), reason='POSIX install script') + + +def install(tmp_path, system, tools=None, fail=None, **env): + """Run install_reqs.sh against stub uname/uv/python (and optional GPU tools); return its exit code and uv calls.""" + stubs, log = tmp_path / 'bin', tmp_path / 'uv.log' + stubs.mkdir(parents=True, exist_ok=True) + for name, body in {'uname': f'echo {system}', 'python': 'exit 0', 'nvidia-smi': 'exit 9', # a failing nvidia-smi is no GPU + 'uv': f'echo "$*" >> "{log}"' + (f'; case "$*" in *{fail}*) exit 1;; esac' if fail else ''), **(tools or {})}.items(): + (stubs / name).write_text('#!/bin/sh\n' + body + '\n'); (stubs / name).chmod(0o755) + result = subprocess.run(['bash', str(SCRIPT)], capture_output=True, text=True, timeout=30, + env={'PATH': f'{stubs}:/usr/bin:/bin', 'HOME': str(tmp_path), 'PYTHON': str(stubs / 'python'), **env}) + calls = log.read_text().splitlines() if log.exists() else [] + return result.returncode, [call.split(' --python ')[-1].split(' ', 1)[-1] for call in calls] + + +@pytest.mark.parametrize('system,tools,torch', [ + ('Darwin', {}, 'torch'), + ('Linux', {'nvidia-smi': 'case "$*" in *driver_version*) echo 580.65.06;; esac'}, 'torch --index-url https://download.pytorch.org/whl/cu130'), + ('Linux', {'nvidia-smi': 'case "$*" in *driver_version*) echo 550.54.15;; esac'}, 'torch --index-url https://download.pytorch.org/whl/cu126'), + ('Linux', {'nvidia-smi': 'case "$*" in *driver_version*) echo 580.65.06;; *compute_cap*) echo 6.1;; esac'}, 'torch --index-url https://download.pytorch.org/whl/cu126'), # Pascal: no CUDA 13 kernels + ('Linux', {'nvidia-smi': 'case "$*" in *driver_version*) echo 580.65.06;; *compute_cap*) echo 7.5;; esac'}, 'torch --index-url https://download.pytorch.org/whl/cu130'), + ('Linux', {'rocminfo': 'exit 0'}, 'torch --index-url https://download.pytorch.org/whl/rocm7.2'), + ('Linux', {}, 'torch --index-url https://download.pytorch.org/whl/cpu'), + ('MINGW64_NT-10.0', {'rocminfo': 'exit 0'}, 'torch --index-url https://download.pytorch.org/whl/cpu'), +]) +def test_install_script_picks_torch_wheels_for_the_platform(tmp_path, system, tools, torch): + if system == 'Linux' and not tools and os.path.exists('/opt/rocm/bin/rocminfo'): pytest.skip('host has ROCm') + code, calls = install(tmp_path, system, tools) + assert code == 0 and calls == [torch, '-r requirements-runtime.txt', '--no-deps EfficientWord-Net'] + + +def test_install_script_override_and_failures_stop_it(tmp_path): + assert install(tmp_path / 'a', 'Darwin', RIKO_TORCH_INDEX='cu126') == (0, ['torch --index-url https://download.pytorch.org/whl/cu126', '-r requirements-runtime.txt', '--no-deps EfficientWord-Net']) + assert install(tmp_path / 'b', 'Darwin', RIKO_TORCH_INDEX='https://example.invalid') == (1, []) + # A failed step ends the script instead of continuing into a half-installed environment. + assert install(tmp_path / 'c', 'Darwin', fail='torch') == (1, ['torch']) + assert install(tmp_path / 'd', 'Darwin', tools={'python': 'exit 1'})[0] == 1 + # Blackwell on a pre-580 driver: CUDA 12.6 has no kernels for it, so stop instead of installing a wheel that cannot run. + assert install(tmp_path / 'e', 'Linux', {'nvidia-smi': 'case "$*" in *driver_version*) echo 575.51;; *compute_cap*) echo 12.0;; esac'}) == (1, []) + + +def test_install_script_is_strict_and_installs_nothing_unused(): + text = SCRIPT.read_text(encoding='utf-8') + assert text.startswith('#!/usr/bin/env bash\n') and 'set -euo pipefail' in text and os.access(SCRIPT, os.X_OK) + assert 'torchaudio' not in text and 'nltk' not in text diff --git a/tests/test_llama_native.py b/tests/test_llama_native.py index 66dee1e..966da2a 100644 --- a/tests/test_llama_native.py +++ b/tests/test_llama_native.py @@ -74,7 +74,8 @@ def test_interval_is_ui_slider_and_live_only_change(tmp_path): def test_julia_gpu_option_does_not_enable_gpu_probe(): - assert field('emotion.device', 'cpu')['options'] == ['cpu', 'cuda', 'cuda:0'] + assert field('emotion.device', 'cpu')['options'] == ['cpu', 'auto', 'cuda', 'cuda:0'] + assert field('memory.device', 'cpu')['options'] == ['cpu', 'auto', 'cuda', 'cuda:0', 'mps'] from process.app_core.emotion.probe import ProbeConfig with pytest.raises(ValueError): ProbeConfig.from_raw({'device': 'cuda'}) @@ -299,3 +300,144 @@ def request(handle, path, body, output, cancel, user): assert not options['cancelled']() finally: provider.close() + + +def gguf(path, context_length): + import struct + def text(value): + data = value.encode() + return struct.pack('=0 + + +def test_legacy_faiss_index_file_is_not_offered(store): + assert 'memory.index_file' not in {field['path'] for field in store.snapshot()['fields']} + + +def test_memory_device_is_a_background_model_setting(store): + snapshot = store.snapshot() + spec = next(item for item in snapshot['fields'] if item['path'] == 'memory.device') + assert snapshot['values']['memory.device'] == 'cpu' and spec['section'] == 'Background models' and spec['restart'] + assert store.validate({'memory.device': 'mps', 'emotion.device': 'auto'})['valid'] + assert not store.validate({'memory.device': 'vulkan'})['valid'] + assert not store.validate({'emotion.device': 'mps'})['valid'] + + +def test_flash_attention_is_a_choice_that_keeps_old_boolean_files(store): + snapshot = store.snapshot() + spec = next(item for item in snapshot['fields'] if item['path'] == 'runtime.flash_attn') + assert snapshot['values']['runtime.flash_attn'] == 'off' and spec['kind'] == 'text' and spec['options'] == ['auto', 'on', 'off'] + result = store.save({'runtime.flash_attn': 'on'}, snapshot['revision']) + assert result['saved'] and result['values']['runtime.flash_attn'] == 'on' + assert 'flash_attn: on' in store.path.read_text() # ruamel writes it bare, so PyYAML reads true + from process.app_core.configuration.config import load_config + native = store.path.with_name('native.yaml') + native.write_text('runtime:\n provider: llama_cpp\n model_path: model.gguf\n flash_attn: on\n') + assert load_config(native).runtime.flash_attn == 'on' + assert not store.save({'runtime.flash_attn': True}, store.snapshot()['revision'])['saved'] + + +def test_layer_offload_and_split_choices_match_llama_cpp(store): + assert store.validate({'runtime.n_gpu_layers': -2})['valid'] + assert not store.validate({'runtime.n_gpu_layers': -3})['valid'] + assert not store.validate({'runtime.split_mode': 'row'})['valid'] + store.path.write_text(store.path.read_text().replace('provider: lm_studio', 'provider: llama_cpp') + .replace(' model_path: null', ' model_path: model.gguf').replace(' flash_attn: false', ' flash_attn: false\n split_mode: row')) + assert store.snapshot()['values']['runtime.split_mode'] == 'row' # an old file still opens + assert 'runtime.split_mode' in store.validate({})['errors'] + + +def test_speech_recognition_error_names_the_value_that_cannot_run(store, monkeypatch): + from process.app_core.audio import asr + monkeypatch.setattr(asr, 'supported_types', lambda: {'cpu': frozenset({'float32', 'int8'}), 'cuda': frozenset({'float32', 'int8', 'int8_float16'})}) + store.path.write_text(store.path.read_text().replace(' wake_threshold: 0.9\n', ' wake_threshold: 0.9\n asr_device: cuda\n asr_compute_type: int8_float16\n')) + errors = store.validate({'voice.asr_device': 'cpu'})['errors'] # CPU is fine; the stored precision is what cannot run there + assert 'voice.asr_device' not in errors and 'cpu cannot run int8_float16' in errors['voice.asr_compute_type'] + assert store.validate({'voice.asr_device': 'cpu', 'voice.asr_compute_type': 'default'})['valid'] diff --git a/tests/test_tasks.py b/tests/test_tasks.py index c133f02..6b6342d 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -1,9 +1,12 @@ +import os from pathlib import Path +import re import sys import pytest from process.app_core.persistence.tasks import TaskStore, TaskMCP, TaskConflict +from process.app_core.tools import registry as tool_registry from process.app_core.tools.registry import ToolRegistry, StdioMCPClient from process.app_core.conversation.chat import ChatService from process.app_core.conversation.messages import ChatMessage, ModelResponse, ToolCall @@ -91,6 +94,38 @@ def test_real_stdio_mcp_handshake_and_task_round_trip(tmp_path): client.process.wait(timeout=5) +def test_mcp_command_is_found_on_the_child_path_and_a_miss_names_that_path(tmp_path, monkeypatch): + with pytest.raises(FileNotFoundError, match="'riko-missing-mcp' was not found on PATH: " + re.escape(str(tmp_path))): + StdioMCPClient('riko-missing-mcp', env={'PATH': str(tmp_path)}) + seen = [] # Windows: which() applies PATHEXT, so a bare npx becomes its npx.CMD shim on the merged PATH. + monkeypatch.setattr(tool_registry.shutil, 'which', lambda command, path=None: seen.append((command, path)) or 'C:/node/npx.CMD') + assert tool_registry.resolve_command('npx', {'PATH': os.pathsep.join(['/a', '/b'])}) == 'C:/node/npx.CMD' + assert seen == [('npx', os.pathsep.join(['/a', '/b']))] + assert tool_registry.resolve_command('C:/proj/.venv/Scripts/python', {}) == 'C:/proj/.venv/Scripts/python' # a path runs as written + + +@pytest.mark.skipif(os.name == 'nt', reason='POSIX shell wrapper') +def test_mcp_server_named_by_bare_command_on_a_configured_path(tmp_path): + entry, store = Path(__file__).resolve().parents[1] / 'Code' / 'task_mcp_server.py', tmp_path / 'tasks.sqlite3' + wrapper = tmp_path / 'riko-task-mcp' + wrapper.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{entry}" --store "{store}"\n'); wrapper.chmod(0o755) + client = StdioMCPClient('riko-task-mcp', env={'PATH': str(tmp_path)}) + try: assert 'task_create' in {tool['name'] for tool in client.list_tools()} + finally: client.close() + + +def test_unloadable_mcp_server_is_reported_to_the_desktop(tmp_path): + from types import SimpleNamespace + from process.app_core.desktop.state import get_desktop_state + (tmp_path / 'mcp.json').write_text('{"mcpServers": {"broken": {"command": "riko-missing-mcp"}}}', encoding='utf-8') + config = SimpleNamespace(root=tmp_path, tools=SimpleNamespace(mcp_config=tmp_path / 'mcp.json', timeout_seconds=5, require_approval=True)) + registry = ToolRegistry.from_config(config) + try: + notice = get_desktop_state().notifications[0] + assert notice['source'] == 'tools' and notice['level'] == 'error' and 'broken' in notice['text'] and 'riko-missing-mcp' in notice['text'] + finally: registry.close() + + def test_task_validation_does_not_commit_failed_changes(tmp_path): store = TaskStore(tmp_path / 'tasks.sqlite3') task = store.create('Actual goal') diff --git a/tests/test_torch_device.py b/tests/test_torch_device.py new file mode 100644 index 0000000..76b8e9a --- /dev/null +++ b/tests/test_torch_device.py @@ -0,0 +1,78 @@ +"""Device policy for in-process torch models (Julia, memory classifier, embeddings).""" +import sys +import threading +from types import SimpleNamespace + +import numpy as np +import pytest + +from process.app_core.configuration.config import MemoryConfig +from process.app_core.emotion import JuliaEmotionEngine +from process.app_core.emotion.julia import load_julia +from process.app_core.persistence.memory import MemoryStore +from process.app_core.runtime.torch_device import resolve, validate + + +def fake_torch(cuda, mps): + return SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: cuda), backends=SimpleNamespace(mps=SimpleNamespace(is_available=lambda: mps))) + + +def test_device_names_and_auto_resolution(monkeypatch): + assert [validate(name) for name in ('cpu', 'CUDA', 'cuda:1', 'mps', 'auto')] == ['cpu', 'cuda', 'cuda:1', 'mps', 'auto'] + for name in ('vulkan', 'cuda:', 'rocm', ''): + with pytest.raises(ValueError, match='Unsupported torch device'): validate(name) + for cuda, mps, expected in ((True, True, 'cuda'), (False, True, 'mps'), (False, False, 'cpu')): + monkeypatch.setitem(sys.modules, 'torch', fake_torch(cuda, mps)) + assert resolve('auto') == expected + assert resolve('mps') == 'mps' # Explicit names pass through; loaders handle failure. + assert JuliaEmotionEngine(None, device='mps').device == 'mps' + with pytest.raises(ValueError): JuliaEmotionEngine(None, device='vulkan') + + +def test_julia_load_maps_mps_to_cpu_retries_gpu_on_cpu_and_restores_torch_globals(): + import torch + threads, precision = torch.get_num_threads(), torch.get_float32_matmul_precision() + calls = [] + def load_model(source, device, **options): # what Julia's configure() does on every load + calls.append(device); torch.set_num_threads(1); torch.set_float32_matmul_precision('high') + if device != 'cpu': raise RuntimeError('CUDA requested but unavailable') + return SimpleNamespace(device=device, options=options) + julia = SimpleNamespace(load_model=load_model) + model, device = load_julia(julia, 'snapshot', 'cuda:1', max_length=512) + assert (device, model.options, calls) == ('cpu', {'max_length': 512}, ['cuda:1', 'cpu']) + calls.clear() + assert load_julia(julia, 'snapshot', 'mps')[1] == 'cpu' and calls == ['cpu'] + seen = [] + worker = threading.Thread(target=lambda: seen.append(torch.get_num_threads())); worker.start(); worker.join() + assert torch.get_num_threads() == threads and seen == [threads] + assert torch.get_float32_matmul_precision() == precision + with pytest.raises(RuntimeError): load_julia(SimpleNamespace(load_model=lambda *a, **k: (_ for _ in ()).throw(RuntimeError('bad weights'))), 'snapshot', 'cpu') + assert torch.get_num_threads() == threads + + +def test_memory_embedder_uses_configured_device_with_cpu_retry(tmp_path, monkeypatch): + devices = [] + class SentenceTransformer: + def __init__(self, name, device): + devices.append(device) + if device != 'cpu': raise AssertionError('Torch not compiled with CUDA enabled') + def encode(self, texts, convert_to_numpy): return np.ones((len(texts), 4)) + monkeypatch.setitem(sys.modules, 'sentence_transformers', SimpleNamespace(SentenceTransformer=SentenceTransformer)) + config = MemoryConfig(store_file=tmp_path / 'memories.json', index_file=tmp_path / 'index', system1_enabled=False, device='cuda') + memory = MemoryStore(config, start_worker=False) + try: + assert memory._embed(['hello']).shape == (1, 4) and devices == ['cuda', 'cpu'] + finally: memory.close() + with pytest.raises(ValueError, match='Unsupported torch device'): + MemoryStore(MemoryConfig(store_file=tmp_path / 'other.json', system1_enabled=False, device='gpu'), start_worker=False) + + +def test_an_explicit_julia_thread_count_still_applies_after_loading(monkeypatch): + import torch + from process.app_core.runtime.torch_device import preserve_torch_globals + threads = torch.get_num_threads() + monkeypatch.setenv('JULIA_CPU_THREADS', '2') + try: + with preserve_torch_globals(): torch.set_num_threads(4) # what Julia's configure() does + assert torch.get_num_threads() == 2 + finally: torch.set_num_threads(threads) diff --git a/tools/llama_cpp/riko-native.cpp b/tools/llama_cpp/riko-native.cpp index a1232b1..a1f2bed 100644 --- a/tools/llama_cpp/riko-native.cpp +++ b/tools/llama_cpp/riko-native.cpp @@ -4,14 +4,17 @@ #include "server-context.h" #include "server-http.h" #include "json.h" +#include "log.h" #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -41,15 +44,113 @@ struct riko_runtime { using riko_output = int (*)(int, const char *, size_t, void *); using riko_cancel = int (*)(void *); +namespace { + +// llama.cpp logs why a load failed (OOM, no split buffers, KV type, bad GGUF) instead of returning it. +// While riko_create runs, keep its WARN/ERROR lines so the returned error carries the cause, and the +// layer-offload and flash-attention outcome so a successful create can report them. Every message still +// reaches the normal llama.cpp log (stderr). +struct riko_log_capture { + std::mutex mutex; + bool active = false; + ggml_log_level level = GGML_LOG_LEVEL_NONE; // the level a GGML_LOG_LEVEL_CONT fragment continues + std::string partial; + std::deque> lines; + std::vector notes; +}; + +riko_log_capture & riko_log() { + static riko_log_capture capture; + return capture; +} + +void riko_log_line(riko_log_capture & log, std::string line) { + if (line.size() > 300) line.resize(300); + // The --fit dry run logs below ERROR as DEBUG, so these come from the real load ("enabled" is INFO, "not supported" WARN). + if ((line.rfind("load_tensors: offloaded ", 0) == 0 || line.rfind("resolve_fused_ops: Flash Attention", 0) == 0) && log.notes.size() < 4) { + log.notes.push_back(line); + } + if (log.level == GGML_LOG_LEVEL_INFO) return; + for (const auto & seen : log.lines) if (seen.second == line) return; // --fit loads the model twice + log.lines.emplace_back(log.level, std::move(line)); + if (log.lines.size() > 12) log.lines.pop_front(); +} + +void riko_log_callback(ggml_log_level level, const char * text, void * user_data) { + common_log_default_callback(level, text, user_data); + if (!text) return; + auto & log = riko_log(); + std::lock_guard lock(log.mutex); + if (!log.active) return; + if (level != GGML_LOG_LEVEL_CONT) { + if (!log.partial.empty()) riko_log_line(log, log.partial); + log.partial.clear(); + log.level = level; + } + if (log.level != GGML_LOG_LEVEL_WARN && log.level != GGML_LOG_LEVEL_ERROR && log.level != GGML_LOG_LEVEL_INFO) return; + log.partial += text; + for (size_t end; (end = log.partial.find('\n')) != std::string::npos; log.partial.erase(0, end + 1)) { + riko_log_line(log, log.partial.substr(0, end)); + } + if (log.partial.size() > 4096) log.partial.clear(); +} + +// Captures for one riko_create call and stops on every exit path. +struct riko_log_scope { + riko_log_scope() { + auto & log = riko_log(); + std::lock_guard lock(log.mutex); + log.active = true; + log.level = GGML_LOG_LEVEL_NONE; + log.partial.clear(); + log.lines.clear(); + log.notes.clear(); + } + ~riko_log_scope() { + auto & log = riko_log(); + std::lock_guard lock(log.mutex); + log.active = false; + } + // The failure, then llama.cpp's errors before its warnings. + std::string explain(const char * message) const { + auto & log = riko_log(); + std::lock_guard lock(log.mutex); + if (!log.partial.empty()) riko_log_line(log, log.partial); + std::string result(message); + if (!log.lines.empty()) result += "; llama.cpp reported:"; + for (const auto level : {GGML_LOG_LEVEL_ERROR, GGML_LOG_LEVEL_WARN}) { + for (const auto & line : log.lines) if (line.first == level) result += "\n" + line.second; + } + return result; + } + std::string notes() const { + auto & log = riko_log(); + std::lock_guard lock(log.mutex); + std::string result; + for (const auto & note : log.notes) result += (result.empty() ? "" : "\n") + note; + return result; + } +}; + +void riko_error(char * error, size_t capacity, const std::string & message) { + if (!error || capacity == 0) return; + const size_t length = std::min(capacity - 1, message.size()); + std::memcpy(error, message.data(), length); + error[length] = 0; +} + +} // namespace + extern "C" { RIKO_EXPORT void * riko_create(const char * arguments, int interval, char * error, size_t capacity) { + riko_log_scope scope; try { if (!arguments) { throw std::runtime_error("missing native arguments"); } static std::once_flag initialized; - std::call_once(initialized, []() { common_init(); llama_backend_init(); }); + std::call_once(initialized, []() { common_init(); llama_log_set(riko_log_callback, nullptr); llama_backend_init(); }); const auto input = json::parse(arguments); if (!input.is_array() || input.empty() || input.size() > 256) { throw std::runtime_error("invalid native arguments"); @@ -63,7 +164,7 @@ RIKO_EXPORT void * riko_create(const char * arguments, int interval, char * erro argv.push_back(nullptr); auto runtime = std::make_unique(); if (!common_params_parse((int) strings.size(), argv.data(), runtime->params, LLAMA_EXAMPLE_SERVER)) { - throw std::runtime_error("invalid llama.cpp parameters"); + throw std::runtime_error("llama.cpp rejected the native arguments (its reason is on the backend's stderr)"); } runtime->context.set_emotion_probe_interval(interval); runtime->routes = std::make_unique(runtime->params, runtime->context); @@ -71,6 +172,7 @@ RIKO_EXPORT void * riko_create(const char * arguments, int interval, char * erro throw std::runtime_error("native model initialization failed"); } runtime->routes->update_meta(runtime->context); + riko_error(error, capacity, scope.notes()); // on success the buffer reports the offload and flash attention runtime->loop = std::thread([ptr = runtime.get()]() { try { ptr->context.start_loop(); @@ -83,19 +185,10 @@ RIKO_EXPORT void * riko_create(const char * arguments, int interval, char * erro }); return runtime.release(); } catch (const std::exception & exception) { - if (error && capacity > 0) { - const size_t length = std::min(capacity - 1, std::strlen(exception.what())); - std::memcpy(error, exception.what(), length); - error[length] = 0; - } + riko_error(error, capacity, scope.explain(exception.what())); return nullptr; } catch (...) { - if (error && capacity > 0) { - const char * message = "unknown native initialization failure"; - const size_t length = std::min(capacity - 1, std::strlen(message)); - std::memcpy(error, message, length); - error[length] = 0; - } + riko_error(error, capacity, scope.explain("unknown native initialization failure")); return nullptr; } } diff --git a/tools/release/build.py b/tools/release/build.py index ad5f167..a30f859 100644 --- a/tools/release/build.py +++ b/tools/release/build.py @@ -75,7 +75,7 @@ def main(): '--workpath', ROOT / 'release-build', '--specpath', ROOT / 'release-build', '--collect-submodules', 'process', '--hidden-import', 'desktop_server', '--hidden-import', 'discord_bot', *[item for package in ('torch', 'transformers', 'sentence_transformers', 'faster_whisper', - 'ctranslate2', 'silero_vad', 'onnxruntime', 'faiss', 'sounddevice', 'soundfile', + 'ctranslate2', 'silero_vad', 'onnxruntime', 'sounddevice', 'soundfile', 'scipy', 'cv2', 'ruamel.yaml', 'eff_word_net', 'discord', 'uvicorn') for item in ('--collect-all', package)], ROOT / 'Code/run_server.py') shutil.copytree(STAGE / 'riko-backend', STAGE / 'backend', dirs_exist_ok=True) From 4f3f6e6050d113cbf20e88b1900cbeeac3e0f2a8 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Mon, 5 Oct 2026 20:33:52 +0530 Subject: [PATCH 09/37] Wave 3, segment 1: packaged first run and shipped-OS fixes - V022: the setup wizard leaves room for the reply (memory budget is n_ctx - max_output), llama_cpp configs without a memory key default the same way (a legacy preset key sets n_ctx only), and Settings checks the budget only when a budget key changes, so existing installs can save. - V025: a second packaged launch stops evaluating main.cjs right after losing the single-instance lock, so it cannot spawn another backend. - V027: Setup refuses a data folder equal to or inside the install folder, which updates and uninstall delete. - V030: quitting with unsaved Settings asks first, before the backend is stopped; a reload asks instead of silently cancelling, and the renderer's guard follows real edits only. - V031: the setup and training-data windows get an opaque dark background. - V020: the tool worker, MCP children (PYTHONIOENCODING) and the frozen backend (PyInstaller UTF-8 mode) use UTF-8 stdio on Windows. - V029: a pending approval takes the overlay's pointer only while hovered. - V028: Linux emulates click-through forwarding by polling the cursor for ignoring windows not covered by another Riko window, and runs under XWayland (deb/AppImage launcher args keep --no-sandbox; direct Wayland launches relaunch once with --ozone-platform=x11). - Whiteboard: a fresh board no longer throws on its first restore. - release.yml installs frontend dependencies before the Python checks. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 4 +- Code/process/app_core/configuration/config.py | 7 ++- .../app_core/configuration/settings_store.py | 7 ++- Code/process/app_core/tools/registry.py | 9 ++-- Code/process/app_core/tools/worker.py | 3 ++ Code/run_server.py | 2 + electron/electron-builder.yml | 5 ++ electron/main.cjs | 42 ++++++++++++----- electron/overlay_input.cjs | 14 +++++- electron/preload.cjs | 5 +- electron/release.cjs | 13 ++++-- electron/src/feedback.test.mjs | 11 +++++ electron/src/main.jsx | 1 + electron/src/overlay_input.test.mjs | 18 +++++++- electron/src/release.test.mjs | 22 ++++++++- electron/src/settings_model.mjs | 7 +++ electron/src/settings_model.test.mjs | 13 +++++- electron/src/settings_page.jsx | 12 +++-- electron/src/subscription_guard.test.mjs | 13 ++++-- electron/src/tool_approvals.jsx | 4 +- electron/src/whiteboard.jsx | 2 +- electron/src/window_chrome.test.mjs | 13 ++++++ tests/test_persistence_recovery.py | 21 +++++++++ tests/test_release_build.py | 7 +++ tests/test_settings_store.py | 46 +++++++++++++++++++ tests/test_tasks.py | 23 ++++++++++ tools/release/build.py | 4 +- 27 files changed, 290 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d76b00e..a71793b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,9 @@ jobs: python -m pip install torch --index-url https://download.pytorch.org/whl/cpu python -m pip install -r requirements-runtime.txt pyinstaller==6.12.0 pytest python -m pip install EfficientWord-Net --no-deps + - name: Frontend dependencies + working-directory: electron + run: npm ci - name: Backend release regression checks env: RIKO_RELEASE_BUILD: '1' @@ -61,7 +64,6 @@ jobs: - name: Frontend dependencies and tests working-directory: electron run: | - npm ci npm test npm run build npm install --no-save --package-lock=false electron-builder@26.0.12 diff --git a/Code/process/app_core/configuration/config.py b/Code/process/app_core/configuration/config.py index e708264..6b8973e 100644 --- a/Code/process/app_core/configuration/config.py +++ b/Code/process/app_core/configuration/config.py @@ -217,6 +217,11 @@ def load_config(path: str | Path | None = None) -> AppConfig: runtime.initiative_max_output_tokens = persisted_output except (OSError, ValueError, TypeError, AttributeError): pass runtime.reflection_n_ctx = memory_raw.get('reflection_context_window_tokens', 4096) + # The in-process context holds the prompt and the reply, so by default the prompt gets what the reply leaves. + native = str(runtime.provider).lower().replace('-', '_') == 'llama_cpp' + live_prompt = max(1, runtime.n_ctx - runtime.max_output_tokens) if native else 8192 + # The legacy preset key sets n_ctx; as a prompt budget it would leave no room for the reply. + legacy_prompt = params.get('context_window_token_limit', live_prompt) from ..inference.kv_budget import pool_capacity if runtime.kv_pool_auto: runtime.kv_pool_tokens = pool_capacity(runtime) return AppConfig( @@ -233,7 +238,7 @@ def load_config(path: str | Path | None = None) -> AppConfig: ), memory=MemoryConfig( history_file=_path(root, raw.get("history_file", memory_raw.get("history_file", "persistent_memories/chat_history.json"))) or root / "persistent_memories/chat_history.json", - context_window_tokens=int(memory_raw.get("context_window_tokens", params.get("context_window_token_limit", 8192))), + context_window_tokens=int(memory_raw.get("context_window_tokens", min(legacy_prompt, live_prompt) if native else legacy_prompt)), store_file=_path(root, memory_raw.get("store_file", "persistent_memories/memory_store.json")) or root / "persistent_memories/memory_store.json", index_file=_path(root, memory_raw.get("index_file", "persistent_memories/faiss_index.index")) or root / "persistent_memories/faiss_index.index", embedding_model=str(memory_raw.get("embedding_model", "sentence-transformers/all-MiniLM-L6-v2")), diff --git a/Code/process/app_core/configuration/settings_store.py b/Code/process/app_core/configuration/settings_store.py index 63b2b35..5ea90fd 100644 --- a/Code/process/app_core/configuration/settings_store.py +++ b/Code/process/app_core/configuration/settings_store.py @@ -61,6 +61,9 @@ 'presets.default.model_params.context_window_token_limit': (1, 1048576), 'presets.default.model_params.max_output_tokens': (1, 1048576), } +# Settings that change the live conversation budget (memory.context_window_tokens + runtime.max_output_tokens <= n_ctx). +BUDGET_KEYS = {'runtime.provider', 'runtime.n_ctx', 'runtime.max_output_tokens', 'memory.context_window_tokens', + 'presets.default.model_params.context_window_token_limit', 'presets.default.model_params.max_output_tokens'} INTEGER_NULLS = {'runtime.n_threads', 'runtime.n_threads_batch', 'runtime.kv_pool_tokens', 'voice.input_device'} JSON_NULLS = {'runtime.tensor_split'} OPTIONAL_TEXT = {'runtime.model_path', 'runtime.hf_repo_id', 'runtime.hf_filename', 'runtime.chat_format'} @@ -384,8 +387,10 @@ def prepare(self, changes): pool_capacity(rt) if rt.n_ctx <= 0: errors['runtime.n_ctx'] = 'Managed server requires a positive context' if rt.split_mode == 'row': errors['runtime.split_mode'] = 'Row split is not available in this llama.cpp build; choose layer or none' + # Check the live budget only when one of its inputs is edited: the runtime clamps it (factory.py), so a + # config that does not fit, such as one an older setup wizard wrote, never blocks saving other settings. budget = candidate.memory.context_window_tokens + rt.max_output_tokens - if budget > rt.n_ctx: errors['runtime.n_ctx'] = f'Context must fit conversation budget + response ({budget} tokens)' + if budget > rt.n_ctx and BUDGET_KEYS & changes.keys(): errors['runtime.n_ctx'] = f'Context must fit conversation budget + response ({budget} tokens)' except (ValueError, TypeError, KeyError) as exc: errors['__all__'] = str(exc) finally: Path(temporary).unlink(missing_ok=True) return text, output, errors diff --git a/Code/process/app_core/tools/registry.py b/Code/process/app_core/tools/registry.py index 12a7f2f..8ecde25 100644 --- a/Code/process/app_core/tools/registry.py +++ b/Code/process/app_core/tools/registry.py @@ -81,9 +81,11 @@ class StdioMCPClient: def __init__(self, command: str, args: list[str] | None = None, env: dict[str, str] | None = None): self.command, self.args, self.env = command, args, env self._restart_lock = threading.Lock() - environment = {**os.environ, **(env or {})} + # MCP stdio is UTF-8. Python servers on Windows write the ANSI code page unless told otherwise, and one byte that + # does not decode would stop the stderr drain; PYTHONIOENCODING changes only their stdio, not their file encodings. + environment = {'PYTHONIOENCODING': 'utf-8', **os.environ, **(env or {})} self.process = subprocess.Popen([resolve_command(command, environment), *(args or [])], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, text=True, encoding='utf-8', bufsize=1, env=environment) + stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace', bufsize=1, env=environment) self._counter = 0 self._lock = threading.Lock() self._responses = queue.Queue(maxsize=256) @@ -238,8 +240,9 @@ def close(self): def _isolated_call(self, tool, arguments): command = [sys.executable, '--tool-worker'] if getattr(sys, 'frozen', False) else [sys.executable, str(Path(__file__).with_name('worker.py'))] + # stderr carries whatever the tool's libraries print, in the ANSI code page on Windows: never fail decoding it. process = subprocess.Popen(command, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8') + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace') with self._execution_lock: if self._closed: process.kill(); process.wait() diff --git a/Code/process/app_core/tools/worker.py b/Code/process/app_core/tools/worker.py index acdda79..c7fddfa 100644 --- a/Code/process/app_core/tools/worker.py +++ b/Code/process/app_core/tools/worker.py @@ -7,6 +7,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3])) if __name__ == '__main__': + # The parent reads and writes UTF-8 (registry.py). Without UTF-8 mode, Windows pipes use the ANSI code page, + # which cannot encode results such as todo_list's emoji. + sys.stdin.reconfigure(encoding='utf-8'); sys.stdout.reconfigure(encoding='utf-8') try: request = json.load(sys.stdin) cls = getattr(importlib.import_module(request['module']), request['class']) diff --git a/Code/run_server.py b/Code/run_server.py index 5bd2209..55300b2 100644 --- a/Code/run_server.py +++ b/Code/run_server.py @@ -48,6 +48,8 @@ def main(): configure_logging(config) # Debug our application, not WebSocket frames. Uvicorn DEBUG dumps every # voice.level/chat.delta packet and can dominate the capture/UI event loop. + # Redirected stdout uses the ANSI code page on Windows (cp932 has no em dash); never let output stop startup. + if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='backslashreplace') print("Riko AI server: http://127.0.0.1:8765 — Ctrl+C to stop", flush=True) import desktop_server # Hold the port before the model loads, so no other process (or account) can listen on it and diff --git a/electron/electron-builder.yml b/electron/electron-builder.yml index d2760d3..2a5534a 100644 --- a/electron/electron-builder.yml +++ b/electron/electron-builder.yml @@ -33,3 +33,8 @@ linux: arch: [x64] category: Utility maintainer: Riko contributors + # The overlay needs window positioning, always-on-top and pointer forwarding, which native Wayland ignores; run under XWayland. + executableArgs: ['--ozone-platform=x11'] # deb launcher +appImage: + # Replaces electron-builder's default '--no-sandbox', which Ubuntu 24.04+ needs for AppImages, so keep it. + executableArgs: ['--no-sandbox', '--ozone-platform=x11'] diff --git a/electron/main.cjs b/electron/main.cjs index 702f3c1..4fc3eee 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,12 +1,17 @@ const { app, BrowserWindow, globalShortcut, ipcMain, dialog, screen, session, shell, Tray, Menu, nativeImage } = require('electron'); // Hardware acceleration is Electron's default; keep WebGL and GPU rasterization enabled. app.commandLine.appendSwitch('enable-gpu-rasterization'); +// Linux Wayland: the overlay needs X11 (placement, always-on-top, click-through) and Ozone is chosen before this file +// runs, so a launch without a platform (a double-clicked AppImage, a terminal) relaunches once under XWayland. +if(process.platform==='linux'&&process.env.WAYLAND_DISPLAY&&!app.commandLine.hasSwitch('ozone-platform')){app.relaunch({...(process.env.APPIMAGE?{execPath:process.env.APPIMAGE}:{}),args:[...process.argv.slice(1),'--ozone-platform=x11']});app.exit(0);return;} const path = require('path'); const fs = require('fs'); const YAML = require('yaml'); let root = app.isPackaged?process.resourcesPath:path.resolve(__dirname, '..'); -let backendProcess,sovitsProcess,setupWindow,shutdownRequested=false; -if(app.isPackaged&&!app.requestSingleInstanceLock())app.quit(); +let backendProcess,sovitsProcess,setupWindow,shutdownRequested=false,settingsDirty=false; +// A second launch only wakes the running instance. app.quit() before ready still runs whenReady callbacks, which +// would spawn a second backend (loading the model again) and GPT-SoVITS, so stop evaluating this file here. +if(app.isPackaged&&!app.requestSingleInstanceLock()){app.quit();return;} app.on('second-instance',()=>{if(setupWindow&&!setupWindow.isDestroyed())setupWindow.focus();else if(control&&!control.isDestroyed())showControls();}); const release=require('./release.cjs'); const {shortcutBindings}=require('./shortcuts.cjs'); @@ -15,7 +20,7 @@ ipcMain.handle('setup-hardware',async event=>{setupSender(event);const hw=await ipcMain.handle('setup-directory',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openDirectory','createDirectory']});return result.canceled?null:result.filePaths[0];}); ipcMain.handle('setup-model',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openFile'],filters:[{name:'GGUF models',extensions:['gguf']}]});return result.canceled?null:result.filePaths[0];}); ipcMain.handle('setup-sovits',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openFile']});return result.canceled?null:result.filePaths[0];}); -ipcMain.handle('setup-finish',(event,values)=>{setupSender(event);const directory=release.saveSetup(values.directory,values.settings,process.resourcesPath);fs.writeFileSync(path.join(app.getPath('userData'),'data-location.json'),JSON.stringify({directory}));app.relaunch();app.quit();return true;}); +ipcMain.handle('setup-finish',(event,values)=>{setupSender(event);const directory=release.saveSetup(values.directory,values.settings,process.resourcesPath,release.installRoot(app.getPath('exe')));fs.writeFileSync(path.join(app.getPath('userData'),'data-location.json'),JSON.stringify({directory}));app.relaunch();app.quit();return true;}); const brandIcon=path.join(root,'assets','tray.png'); let debug = false; let overlay, control, whiteboard, effects; @@ -23,10 +28,19 @@ let neuralDataWindow; ipcMain.handle('neural-data-open',event=>{ if(!control||control.isDestroyed()||event.sender.id!==control.webContents.id)throw new Error('Settings renderer required'); if(neuralDataWindow&&!neuralDataWindow.isDestroyed()){neuralDataWindow.show();neuralDataWindow.focus();return true;} - neuralDataWindow=new BrowserWindow({width:1000,height:800,title:'Expression training data',webPreferences:{preload:path.join(__dirname,'preload.cjs'),contextIsolation:true,nodeIntegration:false}}); + neuralDataWindow=new BrowserWindow({width:1000,height:800,title:'Expression training data',backgroundColor:'#101116',webPreferences:{preload:path.join(__dirname,'preload.cjs'),contextIsolation:true,nodeIntegration:false}}); protectNavigation(neuralDataWindow);neuralDataWindow.loadURL(page('neural-data'));return true; }); -const overlayPointer=require('./overlay_input.cjs').overlayInput(enabled=>{if(overlay&&!overlay.isDestroyed())overlay.setIgnoreMouseEvents(!enabled,{forward:true});}); +const {overlayInput,pointerForwarder}=require('./overlay_input.cjs'); +const overlayPointer=overlayInput(enabled=>{if(overlay&&!overlay.isDestroyed())overlay.setIgnoreMouseEvents(!enabled,{forward:true});}); +// Linux only: emulate click-through forwarding for the overlay and the dock/compact chat, and, as Electron does, only +// while a window ignores the mouse; a window taking it gets real events (a stale buttons=0 move would end its drags). +let controlIgnoring=false; +// Electron forwards only to the topmost window; the overlay sits under the chat, whiteboard and setup windows. +const coveredByWindow=point=>[!controlIgnoring&&control,whiteboard,setupWindow,neuralDataWindow].some(w=>{if(!w||w.isDestroyed()||!w.isVisible())return false;const b=w.getBounds();return point.x>=b.x&&point.y>=b.y&&point.xscreen.getCursorScreenPoint(),bounds:w=>w.getContentBounds(), + targets:()=>[!overlayPointer.interactive&&!coveredByWindow(screen.getCursorScreenPoint())&&overlay,controlIgnoring&&!windowGesture&&control].filter(w=>w&&!w.isDestroyed()&&w.isVisible()), + send:(w,point)=>w.webContents.send('forwarded-pointer',point)}):null; let tray; let controlMode='full',fullControlBounds,compactControlBounds; let boardMode='full',boardBounds,controlTween,boardTween,collapsedControlBounds; @@ -123,6 +137,9 @@ function createWindows(characterName='') { if(process.platform==='win32')control.hookWindowMessage(0x0202,()=>{if(windowGesture?.window===control)endWindowGesture();}); control.webContents.once('did-finish-load',()=>{if(!debug){const a=screen.getPrimaryDisplay().workArea;setControlMode('collapsed',{x:a.x+a.width-80,y:a.y+a.height-70},true);}}); control.on('close', event => {if (!app.isQuitting) {event.preventDefault();setControlMode(controlMode==='full'?'compact':'collapsed');}}); + // Electron cancels a vetoed unload silently. before-quit already asked about unsaved Settings; a reload asks here. + control.webContents.on('will-prevent-unload',event=>{if(app.isQuitting||dialog.showMessageBoxSync(control,{type:'question',buttons:['Discard changes','Keep editing'],defaultId:1,cancelId:1,message:'Discard unsaved settings changes?'})===0){settingsDirty=false;event.preventDefault();}}); + control.webContents.on('render-process-gone',()=>{settingsDirty=false;}); control.on('hide',()=>{if(controlMode==='compact')compactControlBounds=control.getBounds();}); control.on('minimize',()=>{if(app.isQuitting)return;control.setFocusable(false);control.restore();setControlMode('collapsed');}); whiteboard = new BrowserWindow({width: 900, height: 700, minWidth: 320, minHeight: 240, show: false, frame:false, transparent:true, backgroundColor:'#00000000', skipTaskbar:true, alwaysOnTop:true, title: characterName?characterName+' — Whiteboard':'Whiteboard', webPreferences: {preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false}}); @@ -161,7 +178,7 @@ function setControlMode(mode,anchor,instant=false){ if(control.isMaximized())control.unmaximize(); const safeAnchor=anchor&&Number.isFinite(anchor.x)&&Number.isFinite(anchor.y)?anchor:undefined; const target=chatBounds(mode,current,area,safeAnchor,mode==='full'?fullControlBounds:mode==='collapsed'?collapsedControlBounds:compactControlBounds); - controlMode=mode;control.setMinimumSize(88,88);control.setResizable(mode==='full');control.setAspectRatio(0);control.setFocusable(mode!=='collapsed');control.setAlwaysOnTop(true);control.setIgnoreMouseEvents(mode!=='full',{forward:true}); + controlMode=mode;control.setMinimumSize(88,88);control.setResizable(mode==='full');control.setAspectRatio(0);control.setFocusable(mode!=='collapsed');control.setAlwaysOnTop(true);control.setIgnoreMouseEvents(controlIgnoring=mode!=='full',{forward:true}); applyWindowMaterial(control,mode,fullControlBlur); control.webContents.send('window-state',{maximized:false,mode,dockWidth:mode==='collapsed'?target.width:collapsedControlBounds?.width||88});tweenBounds(control,target,'chat',instant); } @@ -191,7 +208,8 @@ app.whenReady().then(async () => { if(app.isPackaged){ const locator=path.join(app.getPath('userData'),'data-location.json'); if(!fs.existsSync(locator)){ - setupWindow=new BrowserWindow({width:1000,height:850,webPreferences:{preload:path.join(__dirname,'preload.cjs'),contextIsolation:true,nodeIntegration:false}}); + // Standalone pages paint no canvas (morph.css keeps html/body transparent), so the window supplies style.css's --bg. + setupWindow=new BrowserWindow({width:1000,height:850,backgroundColor:'#101116',webPreferences:{preload:path.join(__dirname,'preload.cjs'),contextIsolation:true,nodeIntegration:false}}); protectNavigation(setupWindow);await setupWindow.loadURL(page('setup'));return; } root=JSON.parse(fs.readFileSync(locator,'utf8')).directory; @@ -215,7 +233,7 @@ app.whenReady().then(async () => { catch(error){dialog.showErrorBox('GPT-SoVITS could not start',error.message);} } debug = config.desktop?.debug === true; - prepareAvatarAssets(); createWindows(config.presets?.default?.name || ''); + prepareAvatarAssets(); createWindows(config.presets?.default?.name || ''); linuxPointer?.start(); createTray(config.presets?.default?.name || ''); for (const name of ['display-added', 'display-removed', 'display-metrics-changed']) { screen.on(name, () => {displaySignature = ''; publishDisplays();}); @@ -245,13 +263,15 @@ app.on('gpu-info-update',()=>publishProcesses()); app.on('child-process-gone',()=>publishProcesses()); ipcMain.on('sync-processes',event=>{if([control,overlay,whiteboard,effects].some(window=>window&&!window.isDestroyed()&&window.webContents.id===event.sender.id))publishProcesses(true);}); app.on('will-quit', () => {globalShortcut.unregisterAll();publishProcesses(true,true);}); -app.on('before-quit', event => {app.isQuitting = true;if(backendProcess&&!shutdownRequested){event.preventDefault();shutdownRequested=true;release.stopBackend(backendProcess).finally(()=>app.quit());}if(sovitsProcess)sovitsProcess.kill();endWindowGesture(); clearTimeout(geometryTimer);clearTimeout(controlTween);clearTimeout(boardTween);}); +// Ask before anything stops: once quitting, the backend is stopped first and unsaved Settings are discarded. +app.on('before-quit', event => {if(!app.isQuitting&&settingsDirty&&control&&!control.isDestroyed()){setControlMode('full',undefined,true);if(dialog.showMessageBoxSync(control,{type:'question',buttons:['Quit and discard','Keep editing'],defaultId:1,cancelId:1,message:'Quit and discard unsaved settings changes?'})!==0){event.preventDefault();return;}}app.isQuitting = true;if(backendProcess&&!shutdownRequested){event.preventDefault();shutdownRequested=true;release.stopBackend(backendProcess).finally(()=>app.quit());}if(sovitsProcess)sovitsProcess.kill();linuxPointer?.stop();endWindowGesture(); clearTimeout(geometryTimer);clearTimeout(controlTween);clearTimeout(boardTween);}); +ipcMain.on('settings-dirty',(event,dirty)=>{if(control&&!control.isDestroyed()&&event.sender.id===control.webContents.id)settingsDirty=dirty===true;}); ipcMain.on('show-control', event => {if([overlay,control,whiteboard].some(w=>w&&!w.isDestroyed()&&w.webContents.id===event.sender.id))showControls();}); function chromeWindow(event){const window=[control,whiteboard].find(w=>w&&!w.isDestroyed()&&w.webContents.id===event.sender.id);if(!window)throw new Error('Control or whiteboard renderer required');return window;} ipcMain.handle('window-state',event=>{const w=chromeWindow(event);return {maximized:w.isMaximized(),mode:w===control?controlMode:boardMode,dockWidth:collapsedControlBounds?.width||88};}); ipcMain.handle('window-action',(event,action,anchor,instant)=>{const w=chromeWindow(event);if(['close','minimize'].includes(action)){if(w===control)setControlMode(controlMode==='full'?'compact':'collapsed',anchor,instant===true);else setBoardMode('collapsed',instant===true);return {mode:w===control?controlMode:boardMode};}if(w===control&&action==='maximize'&&controlMode!=='full')setControlMode('full',anchor,instant===true);return windowAction(w,action);}); ipcMain.handle('chat-mode',(event,mode,anchor,instant)=>{const w=chromeWindow(event);if(w===control)setControlMode(mode,anchor,instant===true);else setBoardMode(mode,instant===true);return {mode:w===control?controlMode:boardMode};}); -ipcMain.on('compact-interactive',(event,enabled)=>{if(controlMode!=='full'&&event.sender.id===control.webContents.id&&!windowGesture)control.setIgnoreMouseEvents(!enabled,{forward:true});}); +ipcMain.on('compact-interactive',(event,enabled)=>{if(controlMode!=='full'&&event.sender.id===control.webContents.id&&!windowGesture)control.setIgnoreMouseEvents(controlIgnoring=!enabled,{forward:true});}); ipcMain.on('window-gesture',(event,value)=>{ const w=[control,whiteboard].find(w=>w&&!w.isDestroyed()&&w.webContents.id===event.sender.id); if(!w||!value||typeof value.id!=='string'||value.id.length>64)return; @@ -259,7 +279,7 @@ ipcMain.on('window-gesture',(event,value)=>{ if(value.phase!=='begin'||!['move','resize'].includes(value.kind)||(value.kind==='resize'&&(w!==control||controlMode==='full')))return; endWindowGesture();clearTimeout(w===control?controlTween:boardTween);w.webContents.send('window-state',{transitioning:false}); const g={id:value.id,window:w,kind:value.kind,mode:controlMode,bounds:w.getBounds(),cursor:screen.getCursorScreenPoint(),began:Date.now()}; - windowGesture=g;w.setIgnoreMouseEvents(false); + windowGesture=g;w.setIgnoreMouseEvents(false);if(w===control)controlIgnoring=false; function frame(){ if(windowGesture!==g)return; if(w.isDestroyed()||Date.now()-g.began>60000){endWindowGesture();return;} diff --git a/electron/overlay_input.cjs b/electron/overlay_input.cjs index 76a9a85..4bfb210 100644 --- a/electron/overlay_input.cjs +++ b/electron/overlay_input.cjs @@ -7,4 +7,16 @@ function overlayInput(apply){ press(){buttons=1;},release(){buttons=0;}, get interactive(){return current;},get buttons(){return buttons;},get dragging(){return drag.size>0;}}; } -module.exports={overlayInput}; +// Linux has no click-through forwarding (setIgnoreMouseEvents' forward option is macOS/Windows only), so a window +// that ignores the mouse never sees the moves that would make it interactive. While the cursor moves, hand its +// position to each click-through window under it; the renderer replays it to its usual hit tests (preload.cjs). +function pointerForwarder({cursor,targets,bounds,send,interval=33,schedule=setTimeout,cancel=clearTimeout}){ + let timer=null,last=null; + function tick(){ + timer=schedule(tick,interval); + const point=cursor();if(last&&point.x===last.x&&point.y===last.y)return;last=point; + for(const target of targets()){const b=bounds(target);if(point.x>=b.x&&point.y>=b.y&&point.xipcRenderer.invoke( contextBridge.exposeInMainWorld('gestureBridge',{send:value=>ipcRenderer.send('window-gesture',value)}); contextBridge.exposeInMainWorld('avatarCursorBridge',{position:()=>ipcRenderer.invoke('avatar-cursor')}); contextBridge.exposeInMainWorld('overlayInputBridge',{native:process.platform==='win32',drag:(source,active)=>ipcRenderer.send('overlay-drag',{source,active}),position:()=>ipcRenderer.invoke('avatar-cursor'),subscribe:callback=>{const listener=(_event,value)=>callback(value);ipcRenderer.on('overlay-pointer',listener);return()=>ipcRenderer.removeListener('overlay-pointer',listener);}}); +// Linux: main forwards the cursor over a click-through window (pointerForwarder in overlay_input.cjs); replay it as +// the pointermove that the avatar, popup, approval and compact-chat hit tests listen for. +if(process.platform==='linux')ipcRenderer.on('forwarded-pointer',(_event,point)=>{if(!Number.isFinite(point?.x)||!Number.isFinite(point?.y))return;(document.elementFromPoint(point.x,point.y)||document.documentElement).dispatchEvent(new PointerEvent('pointermove',{bubbles:true,composed:true,pointerType:'mouse',clientX:point.x,clientY:point.y}));}); contextBridge.exposeInMainWorld('chatSurfaceBridge',{subscribe:callback=>{const listener=(_event,value)=>callback(value);ipcRenderer.on('chat-surface',listener);return()=>ipcRenderer.removeListener('chat-surface',listener);}}); contextBridge.exposeInMainWorld('popupBridge',{interactive:value=>ipcRenderer.send('popup-interactive',value)}); function micAnchor(){const r=document.querySelector('.dock-mic')?.getBoundingClientRect();return r?.width?{x:window.screenX+r.x+r.width/2,y:window.screenY+r.y+r.height/2}:undefined;} @@ -15,4 +18,4 @@ contextBridge.exposeInMainWorld('controlVisibilityBridge', {get:()=>ipcRenderer. contextBridge.exposeInMainWorld('whiteboardBridge', {capture: rect => ipcRenderer.invoke('capture-whiteboard', rect)}); contextBridge.exposeInMainWorld('processBridge', {sync:()=>ipcRenderer.send('sync-processes')}); contextBridge.exposeInMainWorld('approvalBridge', {interactive: enabled => ipcRenderer.send('approval-interactive', enabled)}); -contextBridge.exposeInMainWorld('riko', {showControl: () => ipcRenderer.send('show-control'), showWhiteboard: () => ipcRenderer.send('show-whiteboard'), syncSurfaces: state => ipcRenderer.send('sync-surfaces', state), displays: () => ipcRenderer.invoke('displays'), avatarInteractive: enabled => ipcRenderer.send('avatar-interactive', enabled), pickPath: options => ipcRenderer.invoke('pick-path', options), confirmChange: challenge => ipcRenderer.invoke('confirm-security-change', challenge), onNavigate: callback => {const listener=(_event,view)=>callback(view);ipcRenderer.on('navigate',listener);return()=>ipcRenderer.removeListener('navigate',listener);}}); +contextBridge.exposeInMainWorld('riko', {showControl: () => ipcRenderer.send('show-control'), showWhiteboard: () => ipcRenderer.send('show-whiteboard'), syncSurfaces: state => ipcRenderer.send('sync-surfaces', state), displays: () => ipcRenderer.invoke('displays'), avatarInteractive: enabled => ipcRenderer.send('avatar-interactive', enabled), pickPath: options => ipcRenderer.invoke('pick-path', options), confirmChange: challenge => ipcRenderer.invoke('confirm-security-change', challenge), settingsDirty: dirty => ipcRenderer.send('settings-dirty', dirty === true), onNavigate: callback => {const listener=(_event,view)=>callback(view);ipcRenderer.on('navigate',listener);return()=>ipcRenderer.removeListener('navigate',listener);}}); diff --git a/electron/release.cjs b/electron/release.cjs index 53aa55a..00acdd9 100644 --- a/electron/release.cjs +++ b/electron/release.cjs @@ -23,14 +23,17 @@ function configuration(input,resources){ if(input.modelPath&&(!path.isAbsolute(input.modelPath)||!input.modelPath.toLowerCase().endsWith('.gguf')||!fs.existsSync(input.modelPath)))throw new Error('Local GGUF does not exist'); return {runtime:{provider:'llama_cpp',native_library:'bundled:'+input.backend,model_path:input.modelPath||null,hf_repo_id:input.repo||null,hf_filename:input.filename||null,hf_revision:input.revision||'main',n_ctx:context,max_output_tokens:output,n_threads:threads,n_gpu_layers:input.cpuOnly?0:-1,parallel_slots:2,flash_attn:'auto',type_k:'f16',type_v:'f16',warmup:false}, presets:{default:{name:input.name||'Riko',system_prompt:input.prompt||'You are a helpful local companion.'}}, - memory:{context_window_tokens:context,default_memories:String(input.memories||'').split('\n').filter(t=>t.trim()).map(text=>({text,memory_type:'factual',importance:.8})),embeddings_enabled:!!input.embeddings,system1_enabled:!!input.julia,reflection_enabled:!!input.reflection}, + memory:{context_window_tokens:context-output,default_memories:String(input.memories||'').split('\n').filter(t=>t.trim()).map(text=>({text,memory_type:'factual',importance:.8})),embeddings_enabled:!!input.embeddings,system1_enabled:!!input.julia,reflection_enabled:!!input.reflection}, emotion:{enabled:!!input.julia,device:'cpu',probe:{enabled:false}},voice:{asr_device:'cpu',asr_compute_type:'int8'},tools:{require_approval:true},initiative:{enabled:false},desktop:{setup_on_startup_error:true}, sovits_ping_config:{auto_start:!!input.sovitsAuto,executable:input.sovitsExecutable||null,arguments:[],url:input.sovitsUrl||'http://127.0.0.1:9880/tts',ref_audio_path:input.referenceAudio||'',prompt_text:input.referenceText||'',text_lang:'en',prompt_lang:'en',sample_rate:32000}}; } -function saveSetup(directory,input,resources){ +// The folder an update replaces and an uninstall deletes (NSIS ends with RMDir /r $INSTDIR): the .app bundle on +// macOS, the executable's folder elsewhere. +function installRoot(executable,platform=process.platform){return platform==='darwin'?path.resolve(executable,'..','..','..'):path.dirname(executable);} +function inside(parent,child){const relative=path.relative(path.resolve(parent),path.resolve(child));return relative===''||relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative);} +function saveSetup(directory,input,resources,install=resources){ if(!path.isAbsolute(directory))throw new Error('Choose an absolute data directory'); - const relative=path.relative(path.resolve(resources),path.resolve(directory)); - if(relative===''||relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative))throw new Error('Choose a data folder outside installed application resources'); + if([install,resources].some(root=>inside(root,directory)))throw new Error('Choose a data folder outside the application folder ('+path.resolve(install)+'); updates and uninstall delete everything inside it'); const config=configuration(input,resources); fs.mkdirSync(directory,{recursive:true}); for(const folder of ['models','persistent_memories','logs'])fs.mkdirSync(path.join(directory,folder),{recursive:true}); @@ -77,4 +80,4 @@ function startSovits(settings){ } // Ask first; the backend gives itself 14 s. uvicorn ignores SIGTERM while already shutting down, so escalate to SIGKILL. function stopBackend(child,grace=15000,force=3000){return new Promise(resolve=>{if(!child||child.exitCode!==null||child.signalCode!==null){resolve();return;}let timer=setTimeout(()=>{child.kill();timer=setTimeout(()=>{child.kill('SIGKILL');resolve();},force);},grace);child.once('exit',()=>{clearTimeout(timer);resolve();});child.stdin.end('shutdown\n');});} -module.exports={hardware,configuration,saveSetup,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,finderPath,LISTENING}; +module.exports={hardware,configuration,saveSetup,installRoot,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,finderPath,LISTENING}; diff --git a/electron/src/feedback.test.mjs b/electron/src/feedback.test.mjs index f05bf25..d1fdfa8 100644 --- a/electron/src/feedback.test.mjs +++ b/electron/src/feedback.test.mjs @@ -57,3 +57,14 @@ test('reply speech clouds stream the current turn and ignore stale snapshots/del state=reduceOverlayReply(state,{type:'state.snapshot',payload:{speech:'old reply',runtime:{generating:false}}}); assert.equal(state.text,'New reply');assert.equal(state.generating,false); }); + +test('a waiting tool approval takes the overlay pointer only while it is over the card',()=>{ + const source=fs.readFileSync(new URL('./tool_approvals.jsx',import.meta.url),'utf8'); + assert.doesNotMatch(source,/approvalBridge\?\.interactive\(!!item\)/); + assert.match(source,/closest\?\.\('\.overlay-approval'\)/); + // Click-through forwarding delivers mousemove (macOS/Windows); the Linux replay delivers pointermove. + assert.match(source,/addEventListener\('mousemove',hit,true\)/);assert.match(source,/addEventListener\('pointermove',hit,true\)/); + const preload=fs.readFileSync(new URL('../preload.cjs',import.meta.url),'utf8'); + assert.match(preload,/if\(process\.platform==='linux'\)ipcRenderer\.on\('forwarded-pointer'/); + assert.match(preload,/elementFromPoint\(point\.x,point\.y\)/); +}); diff --git a/electron/src/main.jsx b/electron/src/main.jsx index bc2a396..9f415fa 100644 --- a/electron/src/main.jsx +++ b/electron/src/main.jsx @@ -43,6 +43,7 @@ function Controls() { useEffect(()=>{let alive=true;window.windowBridge?.state().then(value=>{if(alive){setMode(value.mode||'full');if(value.dockWidth)setDockSize(value.dockWidth);}}).catch(()=>{});const off=window.windowBridge?.subscribe(value=>{if(value.mode)setMode(value.mode);if(value.dockWidth)setDockSize(value.dockWidth);});return()=>{alive=false;off?.();};},[]); const [preferences, updatePreferences] = usePreferences(); const runtime=useRuntime(); + useEffect(()=>{window.riko?.settingsDirty?.(settingsDirty);},[settingsDirty]); function navigate(name){if(view==='settings'&&settingsDirty&&name!==view&&!confirm('Discard unsaved settings changes?'))return;setView(name);} useEffect(()=>window.riko?.onNavigate?.(name=>{if(['chat','workspace','tasks','settings','appearance'].includes(name))navigate(name);}),[view,settingsDirty]); useEffect(()=>{document.title=runtime.character_name||'Conversation';},[runtime.character_name]); diff --git a/electron/src/overlay_input.test.mjs b/electron/src/overlay_input.test.mjs index 9777f4b..93ce70f 100644 --- a/electron/src/overlay_input.test.mjs +++ b/electron/src/overlay_input.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; import {beginPopupDrag,movePopupDrag} from './popup_drag.mjs'; -const {overlayInput}=createRequire(import.meta.url)('../overlay_input.cjs'); +const {overlayInput,pointerForwarder}=createRequire(import.meta.url)('../overlay_input.cjs'); test('drag keeps overlay interactive when hover leaves, without enabling focus',()=>{ const changes=[],input=overlayInput(on=>changes.push(on)); input.hover('avatar',true);input.drag('avatar',true);input.hover('avatar',false); @@ -14,3 +14,19 @@ test('popup drag uses viewport coordinates and clamps to screen',()=>{ const position=movePopupDrag(drag,{x:130,y:140},{width:80,height:60},{width:500,height:400}); assert.equal(position.left,70);assert.equal(position.top,90); }); +test('Linux forwarding hands moved cursor positions to each click-through window under it',()=>{ + let cursor={x:5,y:5},targets=[],timer=null,cancelled=0;const sent=[]; + const overlay={name:'overlay',bounds:{x:0,y:0,width:1920,height:1080}},dock={name:'dock',bounds:{x:1840,y:1010,width:88,height:88}}; + const forward=pointerForwarder({cursor:()=>cursor,targets:()=>targets,bounds:w=>w.bounds,send:(w,point)=>sent.push([w.name,point]), + schedule:fn=>(timer=fn),cancel:()=>{cancelled++;timer=null;}}); + targets=[overlay,dock];forward.start();forward.start(); + assert.deepEqual(sent,[['overlay',{x:5,y:5}]]); + timer();assert.equal(sent.length,1,'an unmoved cursor sends nothing'); + cursor={x:1850,y:1020};timer(); + assert.deepEqual(sent.slice(1),[['overlay',{x:1850,y:1020}],['dock',{x:10,y:10}]]); + targets=[dock];cursor={x:100,y:100};timer();assert.equal(sent.length,3,'a window taking the mouse, or not under the cursor, gets nothing'); + targets=[overlay,dock];cursor={x:1928,y:1098};timer();assert.equal(sent.length,3,'right and bottom edges are outside'); + cursor={x:7,y:9};timer();assert.equal(sent.length,4); + forward.stop();assert.equal(cancelled,1);assert.equal(timer,null); + forward.start();assert.deepEqual(sent.slice(4),[['overlay',{x:7,y:9}]],'a restart forwards the current position again'); +}); diff --git a/electron/src/release.test.mjs b/electron/src/release.test.mjs index c8937c5..147b11f 100644 --- a/electron/src/release.test.mjs +++ b/electron/src/release.test.mjs @@ -6,12 +6,31 @@ import path from 'node:path'; import {createRequire} from 'node:module'; import {EventEmitter} from 'node:events'; const require=createRequire(import.meta.url); -const {configuration,saveSetup,startSovits,stopBackend,watchListening,finderPath,LISTENING}=require('../release.cjs'); +const {configuration,saveSetup,installRoot,startSovits,stopBackend,watchListening,finderPath,LISTENING}=require('../release.cjs'); const YAML=require('yaml'); function fixture(){const root=fs.mkdtempSync(path.join(os.tmpdir(),'riko-release-'));const folder=path.join(root,'native','cuda');fs.mkdirSync(folder,{recursive:true});fs.writeFileSync(path.join(folder,process.platform==='win32'?'riko-native.dll':'libriko-native.so'),'test');return root;} const form={backend:'cuda',repo:'owner/model',filename:'model.gguf',context:8192,output:1024,threads:4,memories:'I like tea.\nMy name is Alex.',sovitsAuto:false}; test('setup separates persistent data from binaries and refuses overwrite',()=>{const root=fixture();const directory=fs.mkdtempSync(path.join(os.tmpdir(),'riko-data-'));try{saveSetup(directory,form,root);const config=YAML.parse(fs.readFileSync(path.join(directory,'character_config.yaml'),'utf8'));assert.equal(config.runtime.native_library,'bundled:cuda');assert.equal(config.memory.default_memories.length,2);assert.equal(config.tools.require_approval,true);assert.equal(config.emotion.probe.enabled,false);assert.deepEqual(config.voice,{asr_device:'cpu',asr_compute_type:'int8'});assert.ok(fs.existsSync(path.join(directory,'models')));assert.throws(()=>saveSetup(directory,form,root),/EEXIST/);assert.throws(()=>saveSetup(root,form,root),/outside/);}finally{fs.rmSync(root,{recursive:true,force:true});fs.rmSync(directory,{recursive:true,force:true});}}); test('setup validates budgets, model and explicit speech consent',()=>{const root=fixture();try{assert.throws(()=>configuration({...form,output:8192},root),/budget/);assert.throws(()=>configuration({...form,filename:'../secret.gguf'},root),/GGUF/);assert.throws(()=>configuration({...form,sovitsAuto:true},root),/GPT-SoVITS/);assert.equal(startSovits({auto_start:false}),null);assert.throws(()=>startSovits({auto_start:true,executable:'relative.exe'}),/missing/);}finally{fs.rmSync(root,{recursive:true,force:true});}}); +test('setup leaves room for the reply inside the live context, as Settings requires',()=>{const root=fixture();try{for(const [context,output] of [[8192,1024],[2048,2047],[131072,64]]){const {runtime,memory}=configuration({...form,context,output},root);assert.equal(runtime.n_ctx,context);assert.equal(runtime.max_output_tokens,output);assert.ok(memory.context_window_tokens>=1);assert.ok(memory.context_window_tokens+runtime.max_output_tokens<=runtime.n_ctx);}}finally{fs.rmSync(root,{recursive:true,force:true});}}); +test('setup refuses a data folder that an update or uninstall would delete',()=>{ + const install=fs.mkdtempSync(path.join(os.tmpdir(),'riko-install-')),resources=path.join(install,'resources'),sibling=install+'-data'; + fs.mkdirSync(path.join(resources,'native','cuda'),{recursive:true});fs.writeFileSync(path.join(resources,'native','cuda',process.platform==='win32'?'riko-native.dll':'libriko-native.so'),'test'); + try{ + for(const directory of [install,path.join(install,'data'),path.join(install,'data','nested'),resources])assert.throws(()=>saveSetup(directory,form,resources,install),/outside the application folder/); + assert.ok(!fs.existsSync(path.join(install,'data'))); + assert.equal(saveSetup(sibling,form,resources,install),sibling);assert.ok(fs.existsSync(path.join(sibling,'character_config.yaml'))); + }finally{fs.rmSync(install,{recursive:true,force:true});fs.rmSync(sibling,{recursive:true,force:true});} + assert.equal(installRoot('/Applications/Riko.app/Contents/MacOS/Riko','darwin'),'/Applications/Riko.app'); + assert.equal(installRoot('/opt/Riko/riko-companion','linux'),'/opt/Riko'); + assert.match(fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8'),/release\.saveSetup\(values\.directory,values\.settings,process\.resourcesPath,release\.installRoot\(app\.getPath\('exe'\)\)\)/); +}); +test('a second packaged launch exits before it can spawn a backend, GPT-SoVITS or windows',()=>{ + const host=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8'); + const lock=host.indexOf("if(app.isPackaged&&!app.requestSingleInstanceLock()){app.quit();return;}"); + assert.ok(lock>0,'the losing instance must stop evaluating main.cjs'); + for(const later of ['app.whenReady(','ipcMain.','app.on(',"require('./release.cjs')",'new BrowserWindow('])assert.ok(host.indexOf(later)>lock,later+' must come after the single-instance exit'); +}); test('main learns the backend holds its port only from a complete marker line',()=>{let calls=0;const watch=watchListening(()=>calls++);watch('Riko AI server: http://127.0.0.1:8765\nRIKO_BACKEND_');assert.equal(calls,0);watch('LISTENING_NOT\n');assert.equal(calls,0);watch(LISTENING+'\r\nINFO later\n');assert.equal(calls,1);watch(LISTENING+'\n');assert.equal(calls,1);const split=watchListening(()=>calls++);for(const part of ['RIKO_BACK','END_LISTEN','ING','\n'])split(Buffer.from(part));assert.equal(calls,2);}); function fakeChild({exitOnShutdown=false,signalCode=null}={}){const child=Object.assign(new EventEmitter(),{exitCode:null,signalCode,signals:[],written:'',kill(signal='SIGTERM'){child.signals.push(signal);return true;}});child.stdin={end(text){child.written+=text;if(exitOnShutdown)setImmediate(()=>child.emit('exit',0,null));}};return child;} test('stopBackend asks for shutdown, then escalates to SIGTERM and SIGKILL',async()=>{const hung=fakeChild();await stopBackend(hung,20,20);assert.equal(hung.written,'shutdown\n');assert.deepEqual(hung.signals,['SIGTERM','SIGKILL']);const quick=fakeChild({exitOnShutdown:true});await stopBackend(quick,20,20);assert.deepEqual(quick.signals,[]);const crashed=fakeChild({signalCode:'SIGABRT'}),started=Date.now();await stopBackend(crashed,1000,1000);assert.ok(Date.now()-started<500);assert.equal(crashed.written,'');}); @@ -21,3 +40,4 @@ test('a Finder-launched macOS backend still finds Homebrew tools for MCP servers assert.equal(finderPath(undefined),'/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'); assert.match(fs.readFileSync(new URL('../release.cjs',import.meta.url),'utf8'),/process\.platform==='darwin'\?\{PATH:finderPath\(process\.env\.PATH\)\}/); }); +test('Linux launches keep the AppImage sandbox flag and run under XWayland',()=>{const builder=YAML.parse(fs.readFileSync(new URL('../electron-builder.yml',import.meta.url),'utf8'));assert.deepEqual(builder.appImage.executableArgs,['--no-sandbox','--ozone-platform=x11']);assert.ok(builder.linux.executableArgs.includes('--ozone-platform=x11'));const main=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8');const relaunch=main.indexOf("app.commandLine.hasSwitch('ozone-platform')");assert.ok(relaunch>0&&relaunch 0 || Object.keys(patch.errors).some(path => path in initial && String(inputs[path] ?? '') !== String(initial[path] ?? '')); +} + // The llama-server command for these settings: each slot needs room for the largest of the live, // initiative and reflection budgets (as the backend checks), on the port in the server address. export function llamaServerCommand(inputs) { diff --git a/electron/src/settings_model.test.mjs b/electron/src/settings_model.test.mjs index 319fb7f..8f29794 100644 --- a/electron/src/settings_model.test.mjs +++ b/electron/src/settings_model.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import {test} from 'node:test'; -import {parseSetting,settingsPatch,inputValues,llamaServerCommand,runtimePresets} from './settings_model.mjs'; +import {parseSetting,settingsPatch,settingsEdited,inputValues,llamaServerCommand,runtimePresets} from './settings_model.mjs'; test('numeric inputs validate immediately and do not coerce blanks to zero',()=>{ const field={kind:'number',integer:true,min:2,max:4}; @@ -38,3 +38,14 @@ test('the llama-server command matches what the backend checks: largest budget p 'llama-server -m model.gguf --port 8081 --parallel 2 --ctx-size 16384 --jinja'); assert.equal(llamaServerCommand({'runtime.parallel_slots':'3','runtime.n_ctx':16384,'runtime.base_url':'not a url'}), 'llama-server -m model.gguf --port 8080 --parallel 3 --ctx-size 49152 --jinja'); }); +test('only user edits count as unsaved; errors the stored config already has do not',()=>{ + const fields=[{path:'runtime.provider',kind:'string',options:['llama_cpp','openai']},{path:'runtime.model_path',kind:'string'},{path:'runtime.hf_repo_id',kind:'string'},{path:'runtime.hf_filename',kind:'string'},{path:'runtime.n_batch',kind:'number',integer:true},{path:'runtime.n_ubatch',kind:'number',integer:true}]; + const snapshot={fields,values:{'runtime.provider':'llama_cpp','runtime.model_path':'','runtime.hf_repo_id':'','runtime.hf_filename':'','runtime.n_batch':256,'runtime.n_ubatch':512}}; + const inputs=inputValues(snapshot); + assert.deepEqual(Object.keys(settingsPatch(fields,snapshot.values,inputs).errors).sort(),['runtime.hf_repo_id','runtime.n_ubatch']); + assert.equal(settingsEdited(snapshot,inputs),false); + assert.equal(settingsEdited(snapshot,{...inputs,'runtime.n_ubatch':'512'}),false); + assert.equal(settingsEdited(snapshot,{...inputs,'runtime.n_batch':'abc'}),true); + assert.equal(settingsEdited(snapshot,{...inputs,'runtime.n_ubatch':'1024'}),true); + assert.equal(settingsEdited(snapshot,{...inputs,'runtime.provider':'openai'}),true); +}); diff --git a/electron/src/settings_page.jsx b/electron/src/settings_page.jsx index 551f040..3319b4f 100644 --- a/electron/src/settings_page.jsx +++ b/electron/src/settings_page.jsx @@ -1,7 +1,7 @@ import React, {useEffect, useMemo, useRef, useState} from 'react'; import {Save, FolderOpen, Check, RefreshCw, AlertCircle, Plus, Trash2} from './ui/icons.jsx'; import {request} from './api.mjs'; -import {inputValues, settingsPatch, runtimePresets, llamaServerCommand} from './settings_model.mjs'; +import {inputValues, settingsPatch, settingsEdited, runtimePresets, llamaServerCommand} from './settings_model.mjs'; import InitiativeSettings from './initiative_settings.jsx'; import DisplaySettings from './display_settings.jsx'; import VoiceInput from './voice_input.jsx'; @@ -50,14 +50,16 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) const parsed=useMemo(()=>snapshot?settingsPatch(snapshot.fields,snapshot.values,inputs):{changes:{},errors:{}},[snapshot,inputs]); const sourceDirty=!!snapshot&&inputs['runtime.provider']==='llama_cpp'&&modelSource!==(snapshot.values['runtime.model_path']?'local':'huggingface'); const dirty=sourceDirty || Object.keys(parsed.changes).length>0 || Object.keys(parsed.errors).length>0; - useEffect(()=>{onDirty?.(dirty);},[dirty,onDirty]); + // Only user edits guard navigation, reload and quit: opening Settings on a config it flags must not block them. + const edited=sourceDirty || (!!snapshot&&settingsEdited(snapshot,inputs,parsed)); + useEffect(()=>{onDirty?.(edited);},[edited,onDirty]); useEffect(()=>()=>onDirty?.(false),[onDirty]); useEffect(()=>{ - if(!dirty)return; + if(!edited)return; const handler=event=>{event.preventDefault();event.returnValue='';}; window.addEventListener('beforeunload',handler); return()=>window.removeEventListener('beforeunload',handler); - },[dirty]); + },[edited]); const errors={...serverErrors,...parsed.errors}; if(inputs['runtime.provider']==='llama_cpp'&&modelSource==='local'&&!inputs['runtime.model_path'])errors['runtime.model_path']='Choose a local GGUF file'; if(inputs['runtime.provider']==='llama_cpp'&&!inputs['runtime.native_library'])errors['runtime.native_library']='Choose a compatible riko-native library, or choose llama_server to use a llama-server you run'; @@ -161,7 +163,7 @@ export default function SettingsPage({preferences, updatePreferences, onDirty}) ; } return
-

Settings

+

Settings

{group==='discord'&&} diff --git a/electron/src/subscription_guard.test.mjs b/electron/src/subscription_guard.test.mjs index 919813f..9aa0607 100644 --- a/electron/src/subscription_guard.test.mjs +++ b/electron/src/subscription_guard.test.mjs @@ -2,8 +2,15 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; test('status consumers and Electron host have no periodic polling timers',()=>{ - for(const file of ['animation_library.jsx','initiative_settings.jsx','voice_input.jsx','tool_approvals.jsx','gpu_resources.jsx','stream_chat.jsx','../main.cjs']){ - const path=file==='../main.cjs'?new URL('../main.cjs',import.meta.url):new URL(file,import.meta.url); - assert.doesNotMatch(readFileSync(path,'utf8'),/\bsetInterval\s*\(/,file); + for(const file of ['animation_library.jsx','initiative_settings.jsx','voice_input.jsx','tool_approvals.jsx','gpu_resources.jsx','stream_chat.jsx','../main.cjs','../overlay_input.cjs']){ + assert.doesNotMatch(readFileSync(new URL(file,import.meta.url),'utf8'),/\bsetInterval\s*\(/,file); } }); +test('the only host cursor poll stands in for click-through forwarding, which Linux lacks',()=>{ + const host=readFileSync(new URL('../main.cjs',import.meta.url),'utf8'); + assert.match(host,/const linuxPointer=process\.platform==='linux'\?pointerForwarder\(/); + assert.equal((host.match(/pointerForwarder\(/g)||[]).length,1); + // Like Electron's forwarding, only windows ignoring the mouse get forwarded moves. + assert.match(host,/targets:\(\)=>\[!overlayPointer\.interactive&&!coveredByWindow\(screen\.getCursorScreenPoint\(\)\)&&overlay,controlIgnoring&&!windowGesture&&control\]/); + assert.match(host,/setIgnoreMouseEvents\(controlIgnoring=mode!=='full'/);assert.match(host,/setIgnoreMouseEvents\(controlIgnoring=!enabled/); +}); diff --git a/electron/src/tool_approvals.jsx b/electron/src/tool_approvals.jsx index 40c5b84..c6458f0 100644 --- a/electron/src/tool_approvals.jsx +++ b/electron/src/tool_approvals.jsx @@ -7,7 +7,9 @@ function useApprovals(){ export function ApprovalBubble({overlay=false}){ const [data,,error,setError]=useApprovals(),[busy,setBusy]=useState(false); const item=data?.pending?.[0]; - useEffect(()=>{if(!overlay)return;window.approvalBridge?.interactive(!!item);return()=>window.approvalBridge?.interactive(false);},[overlay,!!item]); + // The overlay spans the display; holding it interactive while a request waits would swallow every click there. + // Like popups, take the pointer only while it is over the card. + useEffect(()=>{if(!overlay||!item)return;let on=false;const hit=e=>{const next=!!e.target.closest?.('.overlay-approval');if(next!==on){on=next;window.approvalBridge?.interactive(next);}};document.addEventListener('pointermove',hit,true);document.addEventListener('mousemove',hit,true);return()=>{document.removeEventListener('pointermove',hit,true);document.removeEventListener('mousemove',hit,true);window.approvalBridge?.interactive(false);};},[overlay,!!item]); async function decide(approved){setBusy(true);try{await request('/api/tools/approvals/'+item.id,{method:'POST',body:{approved}});setError('');}catch(e){setError(e.message);}finally{setBusy(false);}} if(!item)return null; return ; diff --git a/electron/src/whiteboard.jsx b/electron/src/whiteboard.jsx index 904de63..39e270f 100644 --- a/electron/src/whiteboard.jsx +++ b/electron/src/whiteboard.jsx @@ -70,7 +70,7 @@ export default function Whiteboard({commands = [], pages = ['page-1'], modelPage useEffect(() => { if (loaded && !hydrated.current) { hydrated.current = true; - if (saved.current?.seen === commands.at(-1)?.id) seen.current = saved.current.seen; + if (saved.current?.seen && saved.current.seen === commands.at(-1)?.id) seen.current = saved.current.seen; // no saved view on a fresh board if (saved.current?.page && pages.includes(saved.current.page)) setPage(saved.current.page); else if(!pages.includes(page))setPage(modelPage); } diff --git a/electron/src/window_chrome.test.mjs b/electron/src/window_chrome.test.mjs index e43e2c5..9c43e39 100644 --- a/electron/src/window_chrome.test.mjs +++ b/electron/src/window_chrome.test.mjs @@ -41,3 +41,16 @@ test('authored branding images come from the root assets folder',()=>{ assert.match(branding,/\.\.\/\.\.\/assets\/logo.svg/);assert.match(host,/path.join\(root,'assets','tray.png'\)/); assert.ok(fs.statSync(new URL('../../assets/tray.png',import.meta.url)).size>0); }); +test('quitting never stops at a silent Settings unload veto, and asks before the backend stops',()=>{ + const host=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8'),preload=fs.readFileSync(new URL('../preload.cjs',import.meta.url),'utf8'); + assert.match(host,/control\.webContents\.on\('will-prevent-unload',event=>\{if\(app\.isQuitting\|\|dialog\.showMessageBoxSync\(control,/); + assert.match(host,/ipcMain\.on\('settings-dirty',\(event,dirty\)=>\{if\(control&&!control\.isDestroyed\(\)&&event\.sender\.id===control\.webContents\.id\)/); + const quit=host.slice(host.indexOf("app.on('before-quit'")); + assert.ok(quit.indexOf('settingsDirty') ipcRenderer\.send\('settings-dirty', dirty === true\)/); +}); +test('standalone setup and training-data windows paint the dark page background',()=>{ + const host=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8'),css=fs.readFileSync(new URL('./style.css',import.meta.url),'utf8'); + const bg=css.match(/--bg:(#[0-9a-f]{6})/)[1]; + for(const name of ['setupWindow','neuralDataWindow'])assert.match(host,new RegExp(name+"=new BrowserWindow\\(\\{[^}]*backgroundColor:'"+bg+"'"),name); +}); diff --git a/tests/test_persistence_recovery.py b/tests/test_persistence_recovery.py index 2aa88fc..685cfdd 100644 --- a/tests/test_persistence_recovery.py +++ b/tests/test_persistence_recovery.py @@ -151,6 +151,27 @@ def test_timed_out_shared_tool_blocks_duplicate_retry(): finally: release.set();registry.close() +def test_isolated_tool_round_trips_non_ascii_through_ansi_code_page_pipes(tmp_path,monkeypatch): + # Windows pipes use the ANSI code page unless Python runs in UTF-8 mode; cp1252 cannot encode todo_list's emoji + # and writes bytes such as 0xE9 that are not UTF-8. + (tmp_path/'unicode_tool.py').write_text('''import sys +class Tool: + def __init__(self,config,context): pass + def execute(self,**args): + sys.stderr.buffer.write(b'caf\\xe9 warning\\n'); sys.stderr.flush() + return '\\u2705 Added task: ' + args['task'] +''') + monkeypatch.setenv('PYTHONPATH',str(tmp_path)) + monkeypatch.setenv('PYTHONIOENCODING','cp1252') + registry=ToolRegistry(timeout_seconds=30) + registry.tools['unicode']=RegisteredTool('unicode','',{},None,isolated={'module':'unicode_tool','class':'Tool','config':{}}) + try: + result=registry.execute('unicode',{'task':'Caf\u00e9 \u2014 \U0001F95B'}) + assert not result.is_error, result.content + assert result.content=='\u2705 Added task: Caf\u00e9 \u2014 \U0001F95B' + finally: registry.close() + + def test_isolated_tool_is_killed_at_deadline(tmp_path,monkeypatch): (tmp_path/'slow_tool.py').write_text('''from pathlib import Path import time diff --git a/tests/test_release_build.py b/tests/test_release_build.py index 9906b29..e9d036d 100644 --- a/tests/test_release_build.py +++ b/tests/test_release_build.py @@ -37,3 +37,10 @@ def test_upstream_patch_is_limited_to_probe_and_build_glue(): assert files == {'tools/server/CMakeLists.txt', 'tools/server/server-context.cpp', 'tools/server/server-context.h', 'tools/server/server-task.cpp', 'tools/server/server-task.h'} assert not (root / 'tools/llama_cpp/cuda-infinity.patch').exists() + + +def test_frozen_backend_runs_in_utf8_mode(): + # PyInstaller apps ignore PYTHONUTF8, so the backend and its frozen --tool-worker/--discord-worker children get + # UTF-8 stdio only from this build option (Windows otherwise uses the ANSI code page). + source = (Path(__file__).resolve().parents[1] / 'tools/release/build.py').read_text(encoding='utf-8') + assert "'--python-option', 'X utf8'" in source diff --git a/tests/test_settings_store.py b/tests/test_settings_store.py index 32d33b1..be2d612 100644 --- a/tests/test_settings_store.py +++ b/tests/test_settings_store.py @@ -1,5 +1,9 @@ from pathlib import Path +import json import os +import shutil +import subprocess +import sys import pytest @@ -239,3 +243,45 @@ def test_speech_recognition_error_names_the_value_that_cannot_run(store, monkeyp errors = store.validate({'voice.asr_device': 'cpu'})['errors'] # CPU is fine; the stored precision is what cannot run there assert 'voice.asr_device' not in errors and 'cpu cannot run int8_float16' in errors['voice.asr_compute_type'] assert store.validate({'voice.asr_device': 'cpu', 'voice.asr_compute_type': 'default'})['valid'] + + +def test_live_budget_is_checked_only_when_edited_so_older_setups_can_save(tmp_path, monkeypatch): + # The setup wizard used to write memory.context_window_tokens equal to n_ctx, which never leaves room for the reply. + monkeypatch.setenv('RIKO_BUNDLE_ROOT', str(tmp_path / 'application')) + path = tmp_path / 'character_config.yaml' + path.write_text('runtime:\n provider: llama_cpp\n native_library: bundled:cuda\n hf_repo_id: owner/model\n hf_filename: model.gguf\n' + ' n_ctx: 8192\n max_output_tokens: 1024\nmemory:\n context_window_tokens: 8192\n') + store = SettingsStore(path) + assert store.validate({'speech.max_words': 20}) == {'valid': True, 'errors': {}} + assert 'Context must fit' in store.validate({'runtime.max_output_tokens': 512})['errors']['runtime.n_ctx'] + assert store.validate({'memory.context_window_tokens': 7168})['valid'] + path.write_text(path.read_text().replace('memory:\n context_window_tokens: 8192\n', '')) + assert store.snapshot()['values']['memory.context_window_tokens'] == 7168 # the prompt gets what the reply leaves + assert store.validate({'runtime.n_ctx': 4096})['valid'] + assert 'runtime.n_ctx' in store.validate({'runtime.max_output_tokens': 8192})['errors'] + + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.skipif(not shutil.which('node') or not (ROOT / 'electron' / 'node_modules' / 'yaml').is_dir(), reason='needs node and the Electron dependencies') +def test_packaged_setup_writes_a_config_settings_can_save(tmp_path, monkeypatch): + resources = tmp_path / 'application' + library = resources / 'native' / 'cuda' / ('riko-native.dll' if sys.platform == 'win32' else 'libriko-native.so') + library.parent.mkdir(parents=True); library.write_text('test') + form = {'backend': 'cuda', 'repo': 'owner/model', 'filename': 'model.gguf', 'context': 8192, 'output': 1024, 'threads': 4} + script = "require('./release.cjs').saveSetup(process.argv[1], JSON.parse(process.argv[2]), process.argv[3])" + subprocess.run(['node', '-e', script, str(tmp_path / 'data'), json.dumps(form), str(resources)], cwd=ROOT / 'electron', check=True, timeout=60) + monkeypatch.setenv('RIKO_BUNDLE_ROOT', str(resources)) + store = SettingsStore(tmp_path / 'data' / 'character_config.yaml') + assert store.validate({'runtime.n_ctx': 8192}) == {'valid': True, 'errors': {}} # the wizard's budget fits + assert store.validate({'speech.max_words': 20}) == {'valid': True, 'errors': {}} + + +def test_legacy_preset_context_leaves_room_for_the_reply(tmp_path): + from process.app_core.configuration.config import load_config + path = tmp_path / 'character_config.yaml' + path.write_text('runtime:\n provider: llama_cpp\n model_path: model.gguf\n max_output_tokens: 1024\npresets:\n default:\n model_params:\n context_window_token_limit: 8192\n') + config = load_config(path) + assert config.runtime.n_ctx == 8192 and config.memory.context_window_tokens == 7168 + assert SettingsStore(path).validate({'runtime.max_output_tokens': 512})['valid'] diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 6b6342d..767dd8b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -114,6 +114,29 @@ def test_mcp_server_named_by_bare_command_on_a_configured_path(tmp_path): finally: client.close() +def test_python_mcp_server_gets_utf8_mode_and_undecodable_stderr_never_blocks_it(tmp_path, monkeypatch): + # A Windows server logs in the ANSI code page (0xE9 is cp1252 e-acute). The drain must survive it, or the server + # blocks once the stderr pipe fills. + server = tmp_path / 'server.py' + server.write_text('''import json, os, sys +for line in sys.stdin: + request = json.loads(line) + if 'id' not in request: continue + sys.stderr.buffer.write(b'caf\\xe9\\n' + b'x' * 262144 + b'\\n'); sys.stderr.flush() + result = {'tools': []} if request['method'] == 'tools/list' else {'encoding': os.environ.get('PYTHONIOENCODING'), 'stdout': sys.stdout.encoding} + print(json.dumps({'jsonrpc': '2.0', 'id': request['id'], 'result': result}), flush=True) +''', encoding='utf-8') + monkeypatch.delenv('PYTHONIOENCODING', raising=False) + client = StdioMCPClient(sys.executable, [str(server)]) + try: + assert client.list_tools() == [] + assert client.call('probe', {}) == {'encoding': 'utf-8', 'stdout': 'utf-8'} + finally: client.close() + explicit = StdioMCPClient(sys.executable, [str(server)], env={'PYTHONIOENCODING': 'latin-1'}) + try: assert explicit.call('probe', {}) == {'encoding': 'latin-1', 'stdout': 'iso8859-1'} # a server's own setting wins + finally: explicit.close() + + def test_unloadable_mcp_server_is_reported_to_the_desktop(tmp_path): from types import SimpleNamespace from process.app_core.desktop.state import get_desktop_state diff --git a/tools/release/build.py b/tools/release/build.py index a30f859..7e1ee2d 100644 --- a/tools/release/build.py +++ b/tools/release/build.py @@ -70,7 +70,9 @@ def main(): shutil.copy2(checkout / 'LICENSE', notices / 'llama.cpp-LICENSE') shutil.copy2(ROOT / 'tools/release/README.md', notices / 'distribution-notes.md') # Freeze the backend and dynamic imports; no source config or private assets. - run(sys.executable, '-m', 'PyInstaller', '--noconfirm', '--clean', '--onedir', + # UTF-8 mode for the backend, --tool-worker and --discord-worker: frozen apps ignore PYTHONUTF8, and on Windows + # stdio and open() would otherwise use the ANSI code page (logs/backend-launch.log, tool results). + run(sys.executable, '-m', 'PyInstaller', '--noconfirm', '--clean', '--onedir', '--python-option', 'X utf8', '--name', 'riko-backend', '--paths', ROOT / 'Code', '--distpath', STAGE, '--workpath', ROOT / 'release-build', '--specpath', ROOT / 'release-build', '--collect-submodules', 'process', '--hidden-import', 'desktop_server', '--hidden-import', 'discord_bot', From 4742c48d2e12f3c8c80a4f63dd7a5126833b7871 Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Tue, 6 Oct 2026 01:05:57 +0530 Subject: [PATCH 10/37] Wave 3, segment 2: pinned dependencies and a release check that proves something - V026: electron/package.json keeps only yaml (the one module main-process files require) as a runtime dependency; electron, electron-builder 26.0.12 and the renderer libraries are exact devDependencies, so electron-builder accepts the package and release.yml no longer installs it ad hoc. - V131: Python release dependencies are one hash-locked universal file for Windows, Linux and macOS (tools/release/lock.py via uv; CPU torch), plus a --no-deps lock for EfficientWord-Net; CI installs with --require-hashes. Verified here: the lock installs with pip and the full suite passes on it, and it resolves for x86_64 Linux and Windows. - V032: PyInstaller comes from the lock (6.22.3), and numpy._core._exceptions is a hidden import, so the frozen backend survives numpy 2.4. - V033: the CUDA backend builds with GGML_CUDA_NO_VMM, so riko-native no longer hard-links the NVIDIA driver, and the release check runs in a cleaned environment on GPU-less runners. - V203: --release-check also proves the bundle loads its own libraries and no driver library, imports every lazily imported module and desktop_server, runs a calculator call through the real --tool-worker and dry-runs the Discord worker; without a library it checks the dev tree (and a test does). - A tool worker that prints nothing now reports its exit code and logs stderr. - opencv-python is dropped: nothing imports cv2. - release.yml runs every step under bash, so a failing command fails its step on Windows too. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 16 +- CLAUDE.md | 6 +- Code/discord_bot.py | 5 + Code/process/app_core/tools/registry.py | 5 +- Code/run_server.py | 99 +- electron/package-lock.json | 4612 ++++++++++++++++++- electron/package.json | 33 +- electron/src/release.test.mjs | 9 + pyproject.toml | 2 +- requirements-runtime.txt | 1 - tests/test_dependency_manifests.py | 25 + tests/test_release_build.py | 96 + tools/release/build.py | 31 +- tools/release/lock.py | 30 + tools/release/requirements-build.in | 5 + tools/release/requirements-lock-no-deps.txt | 5 + tools/release/requirements-lock.txt | 2661 +++++++++++ 17 files changed, 7498 insertions(+), 143 deletions(-) create mode 100644 tools/release/lock.py create mode 100644 tools/release/requirements-build.in create mode 100644 tools/release/requirements-lock-no-deps.txt create mode 100644 tools/release/requirements-lock.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a71793b..229b717 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,10 @@ jobs: matrix: os: [windows-2022, ubuntu-22.04] runs-on: ${{ matrix.os }} + defaults: + run: + # bash -eo pipefail stops a step at its first failing command; Windows' default pwsh checks only the last one. + shell: bash steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -48,17 +52,17 @@ jobs: "VULKAN_SDK=C:\VulkanSDK\1.3.296.0" >> $env:GITHUB_ENV "C:\VulkanSDK\1.3.296.0\Bin" >> $env:GITHUB_PATH - name: Python runtime dependencies + # Exactly the versions in the hash-locked files from tools/release/lock.py; torch is the CPU build. run: | - python -m pip install torch --index-url https://download.pytorch.org/whl/cpu - python -m pip install -r requirements-runtime.txt pyinstaller==6.12.0 pytest - python -m pip install EfficientWord-Net --no-deps + python -m pip install --require-hashes -r tools/release/requirements-lock.txt --extra-index-url https://download.pytorch.org/whl/cpu + python -m pip install --require-hashes --no-deps -r tools/release/requirements-lock-no-deps.txt - name: Frontend dependencies working-directory: electron run: npm ci - name: Backend release regression checks env: RIKO_RELEASE_BUILD: '1' - run: python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py -q + run: python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py tests/test_dependency_manifests.py -q - name: Build both native backends and freeze Python run: python tools/release/build.py - name: Frontend dependencies and tests @@ -66,12 +70,12 @@ jobs: run: | npm test npm run build - npm install --no-save --package-lock=false electron-builder@26.0.12 - name: Package installers working-directory: electron env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' - run: npx --no-install electron-builder --config electron-builder.yml --publish never + # The deb target needs a homepage, and electron-builder looks for .git only in electron/. Quoted for pwsh. + run: npx --no-install electron-builder --config electron-builder.yml --publish never "-c.extraMetadata.homepage=${{ github.server_url }}/${{ github.repository }}" - uses: actions/upload-artifact@v4 with: name: Riko-${{ runner.os }}-x64 diff --git a/CLAUDE.md b/CLAUDE.md index f430e67..baea63e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,9 @@ python -u Code/run_server.py # backend; run from the repo root; Ctrl+C stops i HF_HUB_OFFLINE=1 python -m pytest -q python -m pytest tests/test_tasks.py -q python -m pytest tests/test_tasks.py::test_real_stdio_mcp_handshake_and_task_round_trip -q -RIKO_RELEASE_BUILD=1 python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py -q # everything CI runs +RIKO_RELEASE_BUILD=1 python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py tests/test_dependency_manifests.py -q # everything CI runs +python Code/run_server.py --release-check # the frozen build's Python-side self-check, against the dev tree (no native library) +python tools/release/lock.py # after editing requirements-runtime.txt or tools/release/requirements-build.in (needs uv) # Electron, from electron/ (CI uses Node 22) npm ci && npm run build && npm run start # loads dist/; start the backend first @@ -56,7 +58,7 @@ cmake --build .native/llama.cpp/build --config Release --target riko-native -j 2 `-DGGML_CUDA=OFF` gives a CPU-only build on Windows/Linux. On macOS the same configure builds Metal by default (embedded shader library, no full Xcode needed) and produces `libriko-native.dylib`. Point `runtime.native_library` at the built library and keep its llama/ggml libraries beside it. -Release (CI only, `.github/workflows/release.yml`): `python tools/release/build.py` clones llama.cpp into `.native/llama.cpp-release` (it refuses if that exists), builds the `cuda` and `vulkan` backends, freezes `riko-backend` with PyInstaller into `release-stage/` and runs `--release-check`; electron-builder 26.0.12 then packages from `electron/`. +Release (CI only, `.github/workflows/release.yml`): Python dependencies come from the hash-locked `tools/release/requirements-lock*.txt` (CPU torch; PyInstaller pinned there), and Electron's from `electron/package-lock.json` via `npm ci` (only `yaml` is a runtime dependency; electron and electron-builder 26.0.12 are exact devDependencies). `python tools/release/build.py` clones llama.cpp into `.native/llama.cpp-release` (it refuses if that exists), builds the `cuda` (with `GGML_CUDA_NO_VMM`, so no hard link to the NVIDIA driver) and `vulkan` backends, freezes `riko-backend` with PyInstaller into `release-stage/` and runs `riko-backend --release-check ` in a cleaned environment: it checks the five exports, that bundled libraries load from the bundle and no driver library loads, imports every lazily imported module and `desktop_server`, runs a calculator call through the real `--tool-worker`, and dry-runs the Discord worker. electron-builder then packages from `electron/`. ## Architecture diff --git a/Code/discord_bot.py b/Code/discord_bot.py index 37989fc..294e4c8 100644 --- a/Code/discord_bot.py +++ b/Code/discord_bot.py @@ -24,6 +24,10 @@ def main(): os.environ.setdefault('RIKO_DATA_DIR', str(root)) # Where the backend client finds the API token. load_dotenv(root / '.env') # Never search parent/private directories for credentials. settings = DiscordAccess(root).settings() + if '--dry-run' in sys.argv: # run_server.release_check: the worker starts and builds its client, but never logs in + CompanionBot(settings) + print('RIKO_DISCORD_DRY_RUN_OK', flush=True) + return if not settings.token: raise ValueError('Discord_bot_token is required') if not settings.admins: raise ValueError('Configure Discord_admins before starting the bot') logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s') @@ -40,4 +44,5 @@ def main(): name = type(exc).__name__ code = 'login' if name == 'LoginFailure' else 'intents' if name == 'PrivilegedIntentsRequired' else 'dependency' if isinstance(exc, ImportError) else 'backend' if isinstance(exc, TimeoutError) else 'startup' print('RIKO_DISCORD_ERROR:' + code, flush=True) + if '--dry-run' in sys.argv: raise # the release check shows the traceback; the launcher never passes --dry-run raise SystemExit(1) from None diff --git a/Code/process/app_core/tools/registry.py b/Code/process/app_core/tools/registry.py index 8ecde25..1c5f98b 100644 --- a/Code/process/app_core/tools/registry.py +++ b/Code/process/app_core/tools/registry.py @@ -250,10 +250,13 @@ def _isolated_call(self, tool, arguments): self._processes.add(process) try: try: - output, _ = process.communicate(json.dumps({**tool.isolated, 'arguments': arguments}), timeout=self.timeout_seconds) + output, errors = process.communicate(json.dumps({**tool.isolated, 'arguments': arguments}), timeout=self.timeout_seconds) except subprocess.TimeoutExpired: process.kill(); process.communicate() raise TimeoutError('Isolated tool terminated at deadline') + if not output.strip(): # the worker itself failed to start (a frozen build missing a module, say) + logger.warning('Tool worker exited with code %s and no result:\n%s', process.returncode, errors[-4000:]) + raise RuntimeError(f'Tool worker exited with code {process.returncode} and no result') payload = json.loads(output.splitlines()[-1]) if 'error' in payload: raise RuntimeError(payload['error']) return payload['result'] diff --git a/Code/run_server.py b/Code/run_server.py index 55300b2..d0184e5 100644 --- a/Code/run_server.py +++ b/Code/run_server.py @@ -3,11 +3,26 @@ import asyncio import os import logging +import sys import uvicorn from process.app_core.configuration.config import load_config +# Imported lazily or dynamically, so a module or data file the frozen build misses would otherwise fail only on a +# user's machine (tools/release/build.py collects them). +RELEASE_MODULES = ('numpy', 'torch', 'transformers', 'sentence_transformers', 'faster_whisper', 'ctranslate2', 'silero_vad', + 'onnxruntime', 'sounddevice', 'soundfile', 'scipy.signal', 'ruamel.yaml', 'pypdf', 'openai', 'huggingface_hub', 'discord', + 'discord_bot') +# Installed only with an NVIDIA display driver: a bundle that loads it at load time fails everywhere else. +DRIVER_LIBRARIES = {'nvcuda.dll', 'libcuda.so', 'libcuda.so.1', 'libcuda.dylib'} + + +def is_driver(name): + """The NVIDIA driver library under any name it loads by; Linux maps list libcuda.so..""" + import re + return name.lower() == 'nvcuda.dll' or bool(re.fullmatch(r'libcuda\.(dylib|so(\.\d+)*)', name)) + class Server(uvicorn.Server): """uvicorn, except that the active turn ends before requests drain: a reply still generating would hold its @@ -21,18 +36,90 @@ async def shutdown(self, sockets=None): await super().shutdown(sockets=sockets) -def main(): - import sys - if '--release-check' in sys.argv: - import ctypes - library = Path(sys.argv[sys.argv.index('--release-check') + 1]).resolve() +def loaded_libraries(names): + """{name: path} for the shared libraries among `names` that this process has loaded.""" + import ctypes + if os.name == 'nt': + from ctypes import wintypes + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + kernel32.GetModuleHandleW.argtypes, kernel32.GetModuleHandleW.restype = [wintypes.LPCWSTR], wintypes.HMODULE + kernel32.GetModuleFileNameW.argtypes = [wintypes.HMODULE, wintypes.LPWSTR, wintypes.DWORD] + kernel32.GetModuleFileNameW.restype = wintypes.DWORD + found = {} + for name in names: + buffer = ctypes.create_unicode_buffer(32768) + module = kernel32.GetModuleHandleW(name) + if module and kernel32.GetModuleFileNameW(module, buffer, len(buffer)): found[name] = Path(buffer.value) + return found + if sys.platform == 'darwin': + dyld = ctypes.CDLL(None) + dyld._dyld_get_image_name.argtypes, dyld._dyld_get_image_name.restype = [ctypes.c_uint32], ctypes.c_char_p + paths = [os.fsdecode(name) for name in map(dyld._dyld_get_image_name, range(dyld._dyld_image_count())) if name] + else: + with open('/proc/self/maps', encoding='utf-8', errors='surrogateescape') as maps: + paths = [fields[5].rstrip('\n') for fields in (line.split(None, 5) for line in maps) if len(fields) == 6 and fields[5].startswith('/')] + return {Path(path).name: Path(path) for path in paths if Path(path).name in names or is_driver(Path(path).name)} + + +def native_bundle_problems(library, loaded): + """Why a loaded native bundle cannot ship: one of its libraries resolved from elsewhere (a toolkit, SDK or + LD_LIBRARY_PATH that users lack was shadowing it), or the NVIDIA driver library loaded with it.""" + bundle = library.resolve().parent + return [f'{name} was loaded from {path}' for name, path in sorted(loaded.items()) + if is_driver(name) or path.resolve().parent != bundle] + + +def release_check(library=None): + """`--release-check [library]`: can this build do what an installed app needs? tools/release/build.py runs it on the + frozen backend once per native backend, without the build's toolkit paths; tests/test_release_build.py runs it on this + tree without a library. It needs no GPU, audio device, credentials, network or model, and touches no user data.""" + import ctypes + import importlib + import subprocess + import tempfile + if library: + library = Path(library).resolve() directory = os.add_dll_directory(str(library.parent)) if os.name == 'nt' else None try: dll = ctypes.CDLL(str(library)) for symbol in ('riko_create', 'riko_request', 'riko_set_interval', 'riko_stop', 'riko_destroy'): getattr(dll, symbol) - import torch, faster_whisper, sounddevice, silero_vad, onnxruntime finally: if directory: directory.close() + problems = native_bundle_problems(library, loaded_libraries({file.name for file in library.parent.iterdir()} | DRIVER_LIBRARIES)) + if problems: raise SystemExit('The native bundle is not self-contained:\n ' + '\n '.join(problems)) + for module in RELEASE_MODULES: importlib.import_module(module) + from eff_word_net.audio_processing import Resnet50_Arc_loss + from silero_vad import load_silero_vad + Resnet50_Arc_loss(); load_silero_vad() # wake words and voice activity: both models are package data files + previous = os.getcwd() + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as data: + # desktop_server loads the config on import: the defaults, in an empty data root, never a developer's files. + os.environ.update(RIKO_DATA_DIR=data, RIKO_CONFIG=str(Path(data) / 'character_config.yaml')) + os.chdir(data) + try: + import desktop_server # the FastAPI app and the whole app_core import graph + from process.app_core.tools.builtin.scientific_calculator import Tool + from process.app_core.tools.registry import ToolRegistry + registry = ToolRegistry(timeout_seconds=300) # through the real worker: --tool-worker when frozen + try: + registry.register_local(Tool({}, {})) + result = registry.execute(Tool.TOOL_NAME, {'expression': '2**10'}) + finally: registry.close() + if result.is_error or result.content != '1024': raise SystemExit(f'A built-in tool failed in its worker: {result.content}') + # As DiscordLauncher starts it, but --dry-run stops before the credentials check and the login. + script = Path(__file__).with_name('discord_bot.py') + command = [sys.executable, '--discord-worker'] if getattr(sys, 'frozen', False) else [sys.executable, str(script)] + worker = subprocess.run([*command, '--dry-run'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=300) + if worker.returncode or 'RIKO_DISCORD_DRY_RUN_OK' not in worker.stdout: + raise SystemExit(f'The Discord worker failed its dry run (exit {worker.returncode}):\n{worker.stdout}{worker.stderr}') + finally: os.chdir(previous) + print('RIKO_RELEASE_CHECK_OK', flush=True) + + +def main(): + if '--release-check' in sys.argv: + arguments = sys.argv[sys.argv.index('--release-check') + 1:] + release_check(arguments[0] if arguments else None) return if '--tool-worker' in sys.argv: import runpy diff --git a/electron/package-lock.json b/electron/package-lock.json index 04d8974..dd1bdf7 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -8,27 +8,31 @@ "name": "riko-desktop", "version": "0.1.0", "dependencies": { - "@pixiv/three-vrm": "^3.5.5", - "@pixiv/three-vrm-animation": "^3.5.5", - "@vitejs/plugin-react": "^4.3.4", - "electron": "latest", - "katex": "^0.18.10", - "lucide-react": "latest", - "react": "latest", - "react-dom": "latest", - "react-markdown": "^10.1.0", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", - "three": "^0.186.1", - "vite": "^6.4.3", - "yaml": "^2.9.1" + "yaml": "2.9.1" + }, + "devDependencies": { + "@pixiv/three-vrm": "3.5.5", + "@pixiv/three-vrm-animation": "3.5.5", + "@vitejs/plugin-react": "4.7.0", + "electron": "44.4.5", + "electron-builder": "26.0.12", + "katex": "0.18.10", + "lucide-react": "1.48.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "react-markdown": "10.1.0", + "rehype-katex": "7.0.1", + "remark-gfm": "4.0.1", + "remark-math": "6.0.0", + "three": "0.186.1", + "vite": "6.4.3" } }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -43,6 +47,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -52,6 +57,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -82,6 +88,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -91,6 +98,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", @@ -107,6 +115,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.7", @@ -123,6 +132,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -132,6 +142,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -141,6 +152,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -154,6 +166,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -171,6 +184,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -180,6 +194,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -189,6 +204,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -198,6 +214,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -207,6 +224,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", @@ -220,6 +238,7 @@ "version": "7.29.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -235,6 +254,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -250,6 +270,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -265,6 +286,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -279,6 +301,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -297,6 +320,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -306,19 +330,129 @@ "node": ">=6.9.0" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@electron-internal/extract-zip": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=22.12.0" } }, + "node_modules/@electron/asar": { + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", + "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/get": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -335,6 +469,293 @@ "undici": "^7.24.4" } }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", + "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -342,6 +763,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -358,6 +780,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -374,6 +797,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -390,6 +814,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -406,6 +831,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -422,6 +848,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -438,6 +865,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -454,6 +882,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -470,6 +899,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -486,6 +916,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,6 +933,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -518,6 +950,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -534,6 +967,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -550,6 +984,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -566,6 +1001,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -582,6 +1018,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -598,6 +1035,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -614,6 +1052,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -630,6 +1069,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -646,6 +1086,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -662,6 +1103,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -678,6 +1120,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -694,6 +1137,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -710,6 +1154,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -726,6 +1171,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -742,6 +1188,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -751,10 +1198,121 @@ "node": ">=18" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -765,6 +1323,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -775,6 +1334,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -784,18 +1344,75 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -803,6 +1420,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -812,13 +1430,43 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@pixiv/three-vrm": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/@pixiv/three-vrm/-/three-vrm-3.5.5.tgz", - "integrity": "sha512-RPXy7jYAXs704NIpZlosB0U2ENu21G9DrqGWdQgRe8dShaCo1ugpj+6BVPRCy91nt+MPMA96j5rbsSzEl0HlQA==", - "license": "MIT", + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", "dependencies": { - "@pixiv/three-vrm-core": "3.5.5", + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pixiv/three-vrm": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@pixiv/three-vrm/-/three-vrm-3.5.5.tgz", + "integrity": "sha512-RPXy7jYAXs704NIpZlosB0U2ENu21G9DrqGWdQgRe8dShaCo1ugpj+6BVPRCy91nt+MPMA96j5rbsSzEl0HlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pixiv/three-vrm-core": "3.5.5", "@pixiv/three-vrm-materials-hdr-emissive-multiplier": "3.5.5", "@pixiv/three-vrm-materials-mtoon": "3.5.5", "@pixiv/three-vrm-materials-v0compat": "3.5.5", @@ -833,6 +1481,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-animation/-/three-vrm-animation-3.5.5.tgz", "integrity": "sha512-Q0kEtmftxfv0iCUHQkJH2jAr1zmT9ywYXnAzhxnLPMYCsx0Kken3J3U2GJF9PdGZmLlKpoK0LFEXVdD/L7ZA4Q==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/three-vrm-core": "3.5.5", @@ -847,6 +1496,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-core/-/three-vrm-core-3.5.5.tgz", "integrity": "sha512-jjaXZbMVRsk2HAGVxqDLmSJyGOJqbMcJpuTHR1TY/TN1FxiKdAzSqdr/ZUcczYipNf8IL9vDT9NX2XmWj+ybRQ==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrm-0.0": "3.5.5", @@ -860,6 +1510,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-materials-hdr-emissive-multiplier/-/three-vrm-materials-hdr-emissive-multiplier-3.5.5.tgz", "integrity": "sha512-XnHEqpY4ctYMucpeRcptdphOe0RrW7V5fUvojasD8Ja4MTjjrjCNFxsAvp3+HBkPZbkTM2sDCjJBJNzxN1scrA==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": "3.5.5" @@ -872,6 +1523,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-materials-mtoon/-/three-vrm-materials-mtoon-3.5.5.tgz", "integrity": "sha512-7lThHf4OKBOsVenD+R6L6HffNUlKO3Twnhr3+z77MnUsSzsOanjVKJtjSIly9QJ6hgl8YJmI6RnxCKqXcpplog==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrm-0.0": "3.5.5", @@ -885,6 +1537,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-materials-v0compat/-/three-vrm-materials-v0compat-3.5.5.tgz", "integrity": "sha512-NjsTK4P4qdcr+RoL2gEUNgF2CY/aplNvyHHxI99N7D6CxDZ2H93FQ9aIuLCABj5b+4CIvldWMCcnS7YuLIr3Lw==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrm-0.0": "3.5.5", @@ -898,6 +1551,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-node-constraint/-/three-vrm-node-constraint-3.5.5.tgz", "integrity": "sha512-+ShEXioZ5Ctf0EZ5ZViZ4n0jhV/qtJRH0Juw8nRnMNODZXDSr+B3An2lrfRoMhowCzG63GiUG4lXWV9noMXosA==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrmc-node-constraint-1.0": "3.5.5" @@ -910,6 +1564,7 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/three-vrm-springbone/-/three-vrm-springbone-3.5.5.tgz", "integrity": "sha512-GWAWrIegGH/u1ZA+Es03Qzc2yafrQOrhtal9jzUIAgU8Wm2DYuqAABOY+du/9RpBRK8D4T8DBPvRGQqvfrQ/uA==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrm-0.0": "3.5.5", @@ -924,57 +1579,77 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrm-0.0/-/types-vrm-0.0-3.5.5.tgz", "integrity": "sha512-2S/0jswMjJTwL9ky8YzDR0CIaP6ZmE+HFuyW/0h+SA+awELf+J7WGgC8lTLM1MXzokO7Z2YVljU2F0fEhlKDIQ==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0/-/types-vrmc-materials-hdr-emissive-multiplier-1.0-3.5.5.tgz", "integrity": "sha512-IuHw9ehXt2bymqfdFV+f2cCXo8kvJUfh0Zuut8qmKXoEb7HVyLGIQyS4TcQHG5yY22UTWHI1Axo5tQv35lPTgQ==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-materials-mtoon-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-materials-mtoon-1.0/-/types-vrmc-materials-mtoon-1.0-3.5.5.tgz", "integrity": "sha512-GFVuksAJVhrWTXYmI/ucNsE611/d8N6BoZrzhUAAINndt98loyzapW2+DJu+GsUBO5yxd+vLsL0FB16XD9+TGg==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-node-constraint-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-node-constraint-1.0/-/types-vrmc-node-constraint-1.0-3.5.5.tgz", "integrity": "sha512-8EbJ1uzkn9yJQarCChmpG12+mc2c7PimHvQ8H6/tMh/hqp5NtJND0z2lR2KeVxw9A3M+TpF+xgImhLBxGjqwzQ==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-springbone-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-springbone-1.0/-/types-vrmc-springbone-1.0-3.5.5.tgz", "integrity": "sha512-gRQ5Y7PaGiMefvOrJ3hvoSPqpA67jiwyhwh8ItjsPi3yri/Hn3CeXplW3Cbx3ZPbfu+C3Q2MCRa2TqAUOjeMRA==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-springbone-extended-collider-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-springbone-extended-collider-1.0/-/types-vrmc-springbone-extended-collider-1.0-3.5.5.tgz", "integrity": "sha512-+6RxLaAvVguP7upjS/OT4viNPWvac1HBRQY1nmdvfYNQuCMdrMsCZqvy2af0Fcjnda4V9JbKFIpZDC0lrw1+uQ==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-vrm-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-vrm-1.0/-/types-vrmc-vrm-1.0-3.5.5.tgz", "integrity": "sha512-WnemHHo2375BcBdumEhvrupZzsMSzTfdWlokmV9sUX2oC+OAQqJFfBjLF8ppkOhzHpl4MAPJqEfuhBJtNPm9ww==", + "dev": true, "license": "MIT" }, "node_modules/@pixiv/types-vrmc-vrm-animation-1.0": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@pixiv/types-vrmc-vrm-animation-1.0/-/types-vrmc-vrm-animation-1.0-3.5.5.tgz", "integrity": "sha512-pyow6wBLdRZbLQbtHqu8uvYG+NXJbwIiLVK6pM6UvtaYotecOv43t6qIdMLYuieV+F+bEJcdfOLFZZVolrOOcQ==", + "dev": true, "license": "MIT", "dependencies": { "@pixiv/types-vrmc-vrm-1.0": "3.5.5" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -984,6 +1659,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -997,6 +1673,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1010,6 +1687,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1023,6 +1701,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1036,6 +1715,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1049,6 +1729,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1062,6 +1743,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1075,6 +1757,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1088,6 +1771,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1101,6 +1785,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1114,6 +1799,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1127,6 +1813,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1140,6 +1827,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1153,6 +1841,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1166,6 +1855,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1179,6 +1869,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1192,6 +1883,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1205,6 +1897,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1218,6 +1911,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1231,6 +1925,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1244,6 +1939,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1257,6 +1953,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1270,6 +1967,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1283,6 +1981,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1296,16 +1995,54 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -1319,6 +2056,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -1328,6 +2066,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -1338,15 +2077,30 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -1356,36 +2110,68 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/katex": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -1395,43 +2181,79 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "24.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@ungap/structured-clone": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -1448,20 +2270,286 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.0.12.tgz", + "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.2.18", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.7.0", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.0.11", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.0.12", + "electron-builder-squirrel-windows": "26.0.12" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.11.26", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1470,10 +2558,36 @@ "node": ">=6.0.0" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.29.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1503,10 +2617,217 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.0.11.tgz", + "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001813", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001813.tgz", "integrity": "sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1527,16 +2848,35 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1547,6 +2887,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1557,6 +2898,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1567,16 +2909,176 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1587,21 +3089,160 @@ "version": "15.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, "license": "MIT", "engines": { "node": ">=22.12.0" } }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT", "peer": true }, @@ -1609,6 +3250,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1626,6 +3268,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -1635,19 +3278,93 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -1657,10 +3374,165 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.0.12.tgz", + "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron": { "version": "44.4.5", "resolved": "https://registry.npmjs.org/electron/-/electron-44.4.5.tgz", "integrity": "sha512-SjgoaeYsSWZfJzubgQU7juvuXMTvn6/e1gAHdGFA/yuMbpF+I+skYqIJ6DdXBXHeWbkbpNpmwZCTpiivtlWZSw==", + "dev": true, "license": "MIT", "dependencies": { "@electron-internal/extract-zip": "^1.0.1", @@ -1675,16 +3547,162 @@ "node": ">= 22.12.0" } }, + "node_modules/electron-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.0.12.tgz", + "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "dmg-builder": "26.0.12", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", + "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", + "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.440", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz", "integrity": "sha512-SghDzqdJokdz8zP8YNlvS74+CwLRoUPDGvP/gFA+HrKVw+pOshUAgx8pXR37xl/u16zGae746rFNWvZ/22kU2Q==", + "dev": true, "license": "ISC" }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1697,6 +3715,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -1705,10 +3724,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1750,89 +3826,471 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": "*" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-util-from-dom": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "dev": true, "license": "ISC", "dependencies": { "@types/hast": "^3.0.0", @@ -1848,6 +4306,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -1866,6 +4325,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -1882,6 +4342,7 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -1902,6 +4363,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -1915,6 +4377,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -1928,6 +4391,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -1955,6 +4419,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -1971,6 +4436,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -1984,6 +4450,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -1997,26 +4464,229 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, "license": "MIT" }, + "node_modules/ip-address": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -2027,6 +4697,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -2037,30 +4708,73 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2069,16 +4783,108 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2087,10 +4893,25 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -2099,10 +4920,24 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/katex": { "version": "0.18.10", "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz", "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==", + "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -2115,20 +4950,73 @@ "katex": "cli.js" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -2138,25 +5026,118 @@ "version": "1.48.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz", "integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==", + "dev": true, "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2173,6 +5154,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2197,6 +5179,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dev": true, "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -2216,6 +5199,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2233,6 +5217,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2250,6 +5235,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2265,6 +5251,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2282,6 +5269,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2298,6 +5286,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -2317,6 +5306,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -2335,6 +5325,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -2359,6 +5350,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -2377,6 +5369,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2391,6 +5384,7 @@ "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -2412,6 +5406,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.3.tgz", "integrity": "sha512-wgyJtgUkUcdU7zci7uuwc/tzoAhm0TswWhaXuvnWolqug+8jY7bgNkBBL0dYBnHgc/tHL7lCzRdPsdUxUKKBqw==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -2435,6 +5430,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -2448,6 +5444,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2484,6 +5481,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2519,6 +5517,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", @@ -2539,6 +5538,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", @@ -2555,6 +5555,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -2575,6 +5576,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -2593,6 +5595,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -2610,6 +5613,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" @@ -2623,6 +5627,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -2640,6 +5645,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, "license": "MIT", "dependencies": { "@types/katex": "^0.16.0", @@ -2659,6 +5665,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -2668,6 +5675,7 @@ "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -2684,6 +5692,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2705,6 +5714,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2727,6 +5737,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2747,6 +5758,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2769,6 +5781,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2791,6 +5804,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2811,6 +5825,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2830,6 +5845,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2851,6 +5867,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2871,6 +5888,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2890,6 +5908,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2912,6 +5931,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2931,6 +5951,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2947,6 +5968,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2963,6 +5985,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -2982,6 +6005,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -3001,6 +6025,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -3022,6 +6047,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -3044,6 +6070,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -3060,6 +6087,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -3072,43 +6100,422 @@ ], "license": "MIT" }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.97.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.97.0.tgz", + "integrity": "sha512-6Pu8F0eXGZlyz0EepPJrWgWDcBI8rKt2Wj8exOJU1tjVbu/VyZy5ZaLYS5N+Jp/Y3IP3sIRKcIOElTu7uC6KKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/node-releases": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", - "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -3128,12 +6535,14 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, "license": "MIT" }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -3142,16 +6551,87 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3160,10 +6640,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3188,29 +6684,127 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3220,6 +6814,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.28.0" @@ -3232,6 +6827,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -3259,15 +6855,45 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/rehype-katex": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -3287,6 +6913,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -3296,6 +6923,7 @@ "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -3312,6 +6940,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3330,6 +6959,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3346,6 +6976,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3362,6 +6993,7 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -3379,6 +7011,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3390,10 +7023,100 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.63.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -3435,47 +7158,298 @@ "fsevents": "~2.3.2" } }, - "node_modules/scheduler": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", - "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", - "license": "MIT" + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" }, "engines": { - "node": ">=10" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -3486,10 +7460,38 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "dev": true, "license": "MIT", "dependencies": { "style-to-object": "1.0.14" @@ -3499,6 +7501,7 @@ "version": "1.0.14", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, "license": "MIT", "dependencies": { "inline-style-parser": "0.2.7" @@ -3508,6 +7511,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -3516,16 +7520,142 @@ "node": ">= 8.0" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/three": { "version": "0.186.1", "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "dev": true, "license": "MIT" }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3538,10 +7668,31 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -3552,16 +7703,42 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { "version": "7.30.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3572,12 +7749,14 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3593,10 +7772,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/unist-util-find-after": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3611,6 +7817,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -3624,6 +7831,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -3637,6 +7845,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3651,6 +7860,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -3664,6 +7874,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3679,6 +7890,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3689,10 +7901,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3719,10 +7942,51 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3737,6 +8001,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3751,6 +8016,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -3765,6 +8031,7 @@ "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -3835,20 +8102,112 @@ } } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -3866,10 +8225,53 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, "license": "MIT", "funding": { "type": "github", diff --git a/electron/package.json b/electron/package.json index 5a178df..8da05d8 100644 --- a/electron/package.json +++ b/electron/package.json @@ -13,20 +13,23 @@ "start": "electron ." }, "dependencies": { - "@pixiv/three-vrm": "^3.5.5", - "@pixiv/three-vrm-animation": "^3.5.5", - "@vitejs/plugin-react": "^4.3.4", - "electron": "latest", - "katex": "^0.18.10", - "lucide-react": "latest", - "react": "latest", - "react-dom": "latest", - "react-markdown": "^10.1.0", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", - "three": "^0.186.1", - "vite": "^6.4.3", - "yaml": "^2.9.1" + "yaml": "2.9.1" + }, + "devDependencies": { + "@pixiv/three-vrm": "3.5.5", + "@pixiv/three-vrm-animation": "3.5.5", + "@vitejs/plugin-react": "4.7.0", + "electron": "44.4.5", + "electron-builder": "26.0.12", + "katex": "0.18.10", + "lucide-react": "1.48.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "react-markdown": "10.1.0", + "rehype-katex": "7.0.1", + "remark-gfm": "4.0.1", + "remark-math": "6.0.0", + "three": "0.186.1", + "vite": "6.4.3" } } diff --git a/electron/src/release.test.mjs b/electron/src/release.test.mjs index 147b11f..8402a5d 100644 --- a/electron/src/release.test.mjs +++ b/electron/src/release.test.mjs @@ -41,3 +41,12 @@ test('a Finder-launched macOS backend still finds Homebrew tools for MCP servers assert.match(fs.readFileSync(new URL('../release.cjs',import.meta.url),'utf8'),/process\.platform==='darwin'\?\{PATH:finderPath\(process\.env\.PATH\)\}/); }); test('Linux launches keep the AppImage sandbox flag and run under XWayland',()=>{const builder=YAML.parse(fs.readFileSync(new URL('../electron-builder.yml',import.meta.url),'utf8'));assert.deepEqual(builder.appImage.executableArgs,['--no-sandbox','--ozone-platform=x11']);assert.ok(builder.linux.executableArgs.includes('--ozone-platform=x11'));const main=fs.readFileSync(new URL('../main.cjs',import.meta.url),'utf8');const relaunch=main.indexOf("app.commandLine.hasSwitch('ozone-platform')");assert.ok(relaunch>0&&relaunch{ + // electron-builder packs each production dependency into app.asar and refuses electron or itself there; the renderer is bundled into dist/. + const pkg=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')),dir=new URL('..',import.meta.url),{builtinModules}=require('node:module'); + const required=new Set(fs.readdirSync(dir).filter(f=>f.endsWith('.cjs')).flatMap(f=>[...fs.readFileSync(new URL(f,dir),'utf8').matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g)].map(m=>m[1])).filter(n=>!n.startsWith('.')&&n!=='electron'&&!n.startsWith('node:')&&!builtinModules.includes(n))); + assert.deepEqual(Object.keys(pkg.dependencies).sort(),[...required].sort()); + for(const name of ['electron','electron-builder','vite'])assert.ok(pkg.devDependencies[name],name); + for(const [name,version] of Object.entries({...pkg.dependencies,...pkg.devDependencies}))assert.match(version,/^\d+\.\d+\.\d+$/,name); + assert.equal(pkg.devDependencies['electron-builder'],'26.0.12'); +}); diff --git a/pyproject.toml b/pyproject.toml index 1142537..3c0a41e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ discord = ["discord.py>=2.7.1,<3", "python-dotenv>=1.0", "aiohttp>=3.9", "pypdf> # Exactly requirements-runtime.txt (tests/test_dependency_manifests.py keeps them equal). EfficientWord-Net # is installed separately with --no-deps; install_reqs.sh picks the PyTorch wheels for the machine. runtime = ["openai>=1.0", "pyyaml>=6.0", "ruamel.yaml>=0.18", "pypdf>=4", "huggingface_hub", - "sentence-transformers", "opencv-python", "httpx", "numpy", "requests", "sounddevice", "soundfile", "torch", + "sentence-transformers", "httpx", "numpy", "requests", "sounddevice", "soundfile", "torch", "faster-whisper", "silero-vad", "scipy", "pillow>=10", "discord.py>=2.7.1,<3", "python-dotenv>=1.0", "aiohttp>=3.9", "transformers", "fastapi>=0.115", "uvicorn>=0.30", "websockets>=12", "onnxruntime"] test = ["riko-companion[runtime]", "pytest>=8.0"] diff --git a/requirements-runtime.txt b/requirements-runtime.txt index f76d024..559a7c5 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -6,7 +6,6 @@ ruamel.yaml>=0.18 pypdf>=4 huggingface_hub sentence-transformers -opencv-python httpx numpy requests diff --git a/tests/test_dependency_manifests.py b/tests/test_dependency_manifests.py index 89a4b5b..c60e81b 100644 --- a/tests/test_dependency_manifests.py +++ b/tests/test_dependency_manifests.py @@ -19,3 +19,28 @@ def test_runtime_extra_mirrors_requirements_runtime_and_covers_every_extra(): for name in ('llama', 'desktop', 'discord'): assert {requirement(item) for item in extras[name]} <= runtime, name assert {requirement(item) for item in project['dependencies']} <= runtime assert 'riko-companion[runtime]' in extras['test'] + + +def locked(path): + """{name: [(version, hash count)]} from a uv-generated, hash-locked requirements file (one entry per marker).""" + entries = {} + for line in path.read_text(encoding='utf-8').replace('\\\n', ' ').splitlines(): + if line.strip() and not line.lstrip().startswith('#'): + name, version = re.match(r'([A-Za-z0-9._-]+)==(\S+)', line).groups() + entries.setdefault(requirement(name)[0], []).append((version, line.count('--hash=sha256:'))) + return entries + + +def test_release_locks_pin_every_runtime_and_build_requirement_with_hashes(): + # Run `python tools/release/lock.py` after editing requirements-runtime.txt or tools/release/requirements-build.in. + from packaging.specifiers import SpecifierSet + release = ROOT / 'tools/release' + lock, no_deps = locked(release / 'requirements-lock.txt'), locked(release / 'requirements-lock-no-deps.txt') + inputs = [line.split('#')[0].strip() for path in (ROOT / 'requirements-runtime.txt', release / 'requirements-build.in') + for line in path.read_text(encoding='utf-8').splitlines()] + for name, spec in (requirement(line) for line in inputs if line): + assert name in lock, f'{name} is not locked' + assert all(SpecifierSet(spec).contains(version, prereleases=True) for version, _ in lock[name]), (name, spec, lock[name]) + assert {'pyinstaller', 'pyinstaller-hooks-contrib', 'pytest'} <= set(lock) + assert set(no_deps) == {'efficientword-net'} and 'efficientword-net' not in lock + assert all(hashes for entries in (*lock.values(), *no_deps.values()) for _, hashes in entries) diff --git a/tests/test_release_build.py b/tests/test_release_build.py index e9d036d..a202e35 100644 --- a/tests/test_release_build.py +++ b/tests/test_release_build.py @@ -1,6 +1,19 @@ import importlib.util +import json +import os from pathlib import Path, PureWindowsPath import subprocess +import sys + +import run_server + + +def release_build(): + source = Path(__file__).resolve().parents[1] / 'tools/release/build.py' + spec = importlib.util.spec_from_file_location('release_build', source) + build = importlib.util.module_from_spec(spec) + spec.loader.exec_module(build) + return build def test_cmake_source_path_uses_forward_slashes_on_windows(): @@ -44,3 +57,86 @@ def test_frozen_backend_runs_in_utf8_mode(): # UTF-8 stdio only from this build option (Windows otherwise uses the ANSI code page). source = (Path(__file__).resolve().parents[1] / 'tools/release/build.py').read_text(encoding='utf-8') assert "'--python-option', 'X utf8'" in source + + +def test_cuda_bundle_does_not_link_the_nvidia_driver(): + # ggml-cuda otherwise links nvcuda.dll/libcuda.so.1 directly, and the release check could not even load it on a + # runner without an NVIDIA driver (run_server.DRIVER_LIBRARIES fails the check if it is loaded anyway). + source = (Path(__file__).resolve().parents[1] / 'tools/release/build.py').read_text(encoding='utf-8') + assert "'-DGGML_CUDA_NO_VMM=ON'" in source + assert 'env=environment' in source and 'release_check_environment(os.environ)' in source + + +def test_release_check_environment_drops_the_build_toolchain(): + toolkit = os.pathsep.join(['/usr/local/cuda-12.4/bin', '/tmp/vulkan-sdk/x86_64/bin', '/opt/NVIDIA GPU Computing Toolkit/v12.4/bin', '/usr/bin', '']) + environment = release_build().release_check_environment({'PATH': toolkit, 'HOME': '/home/runner', 'LD_LIBRARY_PATH': '/usr/local/cuda/lib64', + 'CUDA_PATH': '/usr/local/cuda', 'CUDA_PATH_V12_4': 'x', 'VULKAN_SDK': '/tmp/vulkan-sdk', 'PYTHONPATH': 'Code', + 'RIKO_CONFIG': 'character_config.yaml', 'RIKO_DATA_DIR': '.'}) + assert environment == {'PATH': '/usr/bin', 'HOME': '/home/runner', 'HF_HUB_OFFLINE': '1', 'TRANSFORMERS_OFFLINE': '1'} + assert release_build().release_check_environment({'Path': '/opt/Cuda/bin' + os.pathsep + '/windows'})['Path'] == '/windows' + + +def test_native_bundle_problems_name_shadowed_and_driver_libraries(tmp_path): + bundle = tmp_path / 'native' / 'cuda' + bundle.mkdir(parents=True) + library = bundle / 'libriko-native.so' + loaded = {'libriko-native.so': library, 'libcublas.so.12': bundle / 'libcublas.so.12'} + assert run_server.native_bundle_problems(library, loaded) == [] + toolkit = tmp_path / 'cuda' / 'lib64' / 'libcublasLt.so.12' + problems = run_server.native_bundle_problems(library, {**loaded, 'libcublasLt.so.12': toolkit, 'libcuda.so.1': bundle / 'libcuda.so.1'}) + assert problems == [f'libcublasLt.so.12 was loaded from {toolkit}', f'libcuda.so.1 was loaded from {bundle / "libcuda.so.1"}'] + + +def test_loaded_libraries_finds_this_process_system_library(): + name = 'kernel32.dll' if os.name == 'nt' else 'libSystem.B.dylib' if sys.platform == 'darwin' else 'libc.so.6' + found = run_server.loaded_libraries({name, 'not-a-loaded-library.so'}) + assert list(found) == [name] and found[name].is_absolute() + + +def test_release_check_passes_on_this_tree_without_a_native_library(): + # The frozen backend runs this same check (tools/release/build.py), so a dynamic import or data file the build + # would miss, or a broken --tool-worker or --discord-worker path, fails here before a release does. + root = Path(__file__).resolve().parents[1] + result = subprocess.run([sys.executable, str(root / 'Code/run_server.py'), '--release-check'], cwd=root, + env=release_build().release_check_environment(os.environ), capture_output=True, text=True, encoding='utf-8', + errors='replace', timeout=900) + assert result.returncode == 0, result.stdout + result.stderr + assert 'RIKO_RELEASE_CHECK_OK' in result.stdout + + +def test_frozen_worker_entry_points_route_through_run_server(tmp_path): + # Frozen, the backend is its own tool and Discord worker (ToolRegistry, DiscordLauncher): the same argv here. + root = Path(__file__).resolve().parents[1] + environment = {**release_build().release_check_environment(os.environ), 'RIKO_DATA_DIR': str(tmp_path)} + server = [sys.executable, str(root / 'Code/run_server.py')] + request = {'module': 'process.app_core.tools.builtin.scientific_calculator', 'class': 'Tool', 'arguments': {'expression': '6*7'}} + tool = subprocess.run([*server, '--tool-worker'], input=json.dumps(request), capture_output=True, text=True, encoding='utf-8', + env=environment, cwd=tmp_path, timeout=300) + assert json.loads(tool.stdout.splitlines()[-1]) == {'result': '42'}, tool.stdout + tool.stderr + discord = subprocess.run([*server, '--discord-worker', '--dry-run'], capture_output=True, text=True, encoding='utf-8', + env=environment, cwd=tmp_path, timeout=300) + assert discord.returncode == 0 and 'RIKO_DISCORD_DRY_RUN_OK' in discord.stdout, discord.stdout + discord.stderr + assert not list(tmp_path.iterdir()) # a dry run writes nothing into the data root + + +def test_frozen_numpy_and_locked_release_dependencies(): + # numpy >= 2.3 imports numpy._core._exceptions only from C, so PyInstaller cannot see it. + root = Path(__file__).resolve().parents[1] + source = (root / 'tools/release/build.py').read_text(encoding='utf-8') + assert "'--hidden-import', 'numpy._core._exceptions'" in source + workflow = (root / '.github/workflows/release.yml').read_text(encoding='utf-8') + assert 'pip install --require-hashes -r tools/release/requirements-lock.txt' in workflow + assert 'pip install --require-hashes --no-deps -r tools/release/requirements-lock-no-deps.txt' in workflow + assert '-r requirements-runtime.txt' not in workflow and 'pyinstaller==' not in workflow + # electron-builder comes from electron/package-lock.json through npm ci, never an ad-hoc install. + assert 'npm install' not in workflow and 'npx --no-install electron-builder' in workflow + + +def test_driver_library_is_recognised_under_its_versioned_linux_name_and_ci_steps_stop_on_failure(): + import run_server + assert all(run_server.is_driver(name) for name in ('nvcuda.dll', 'NVCUDA.DLL', 'libcuda.so', 'libcuda.so.1', 'libcuda.so.550.54.15', 'libcuda.dylib')) + assert not any(run_server.is_driver(name) for name in ('libcudart.so.12', 'libcuda_helper.so', 'cudart64_12.dll')) + library = Path('/bundle/libriko-native.so') + assert run_server.native_bundle_problems(library, {'libcuda.so.550.54.15': Path('/usr/lib/x86_64-linux-gnu/libcuda.so.550.54.15')}) + workflow = (Path(__file__).resolve().parents[1] / '.github/workflows/release.yml').read_text(encoding='utf-8') + assert 'defaults:\n run:\n' in workflow and 'shell: bash' in workflow diff --git a/tools/release/build.py b/tools/release/build.py index 7e1ee2d..0eb0112 100644 --- a/tools/release/build.py +++ b/tools/release/build.py @@ -12,8 +12,21 @@ PIN = 'b92761a515ea31e852e7fbc1fad5f874b46f3718' -def run(*args, cwd=ROOT): - subprocess.run([str(a) for a in args], cwd=cwd, check=True) +def run(*args, cwd=ROOT, env=None): + subprocess.run([str(a) for a in args], cwd=cwd, check=True, env=env) + + +def release_check_environment(environ): + """Users have no CUDA toolkit or Vulkan SDK, so the release check runs without the build's library paths, toolkit + variables and PATH entries: a bundled library that is missing must fail to load, not resolve from the toolchain.""" + removed = {'LD_LIBRARY_PATH', 'LD_PRELOAD', 'DYLD_LIBRARY_PATH', 'DYLD_FALLBACK_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES', + 'PYTHONPATH', 'PYTHONHOME', 'VULKAN_SDK', 'VK_SDK_PATH'} + environment = {key: value for key, value in environ.items() + if key.upper() not in removed and not key.upper().startswith(('CUDA', 'RIKO_'))} + for key in [key for key in environment if key.upper() == 'PATH']: + environment[key] = os.pathsep.join(entry for entry in environment[key].split(os.pathsep) + if entry and not any(name in entry.lower() for name in ('cuda', 'vulkan', 'nvidia'))) + return {**environment, 'HF_HUB_OFFLINE': '1', 'TRANSFORMERS_OFFLINE': '1'} def cmake_file_definition(name, path): @@ -47,7 +60,10 @@ def main(): if backend == 'cuda': # Upstream uses -INFINITY as a max-reduction sentinel. MSVC's # expansion triggers NVCC #221 repeatedly; retain the upstream code. - flags += ['-DCMAKE_CUDA_FLAGS=--diag-suppress=221'] + # Without NO_VMM, ggml-cuda links the driver library (nvcuda.dll, libcuda.so.1) directly, so the bundle + # could not load at all without an NVIDIA driver, as on CI runners; cudart still loads the driver when CUDA + # starts. The cost: the CUDA memory pool grows with cudaMalloc instead of virtual-memory mappings. + flags += ['-DCMAKE_CUDA_FLAGS=--diag-suppress=221', '-DGGML_CUDA_NO_VMM=ON'] if sys.platform != 'win32': flags += ['-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON', '-DCMAKE_INSTALL_RPATH=$ORIGIN'] run('cmake', '-S', checkout, '-B', build, *flags) run('cmake', '--build', build, '--config', 'Release', '--target', 'riko-native', '-j', '2') @@ -76,15 +92,18 @@ def main(): '--name', 'riko-backend', '--paths', ROOT / 'Code', '--distpath', STAGE, '--workpath', ROOT / 'release-build', '--specpath', ROOT / 'release-build', '--collect-submodules', 'process', '--hidden-import', 'desktop_server', '--hidden-import', 'discord_bot', + # numpy >= 2.3 imports this only from C; PyInstaller's own numpy hook lists it from 6.14.1 on. + '--hidden-import', 'numpy._core._exceptions', *[item for package in ('torch', 'transformers', 'sentence_transformers', 'faster_whisper', 'ctranslate2', 'silero_vad', 'onnxruntime', 'sounddevice', 'soundfile', - 'scipy', 'cv2', 'ruamel.yaml', 'eff_word_net', 'discord', 'uvicorn') + 'scipy', 'ruamel.yaml', 'eff_word_net', 'discord', 'uvicorn') for item in ('--collect-all', package)], ROOT / 'Code/run_server.py') shutil.copytree(STAGE / 'riko-backend', STAGE / 'backend', dirs_exist_ok=True) executable = STAGE / 'backend' / ('riko-backend.exe' if sys.platform == 'win32' else 'riko-backend') - for backend in ('cuda', 'vulkan'): + environment = release_check_environment(os.environ) + for backend in ('cuda', 'vulkan'): # one process each: both bundles name their libraries alike library = STAGE / 'native' / backend / ('riko-native.dll' if sys.platform == 'win32' else 'libriko-native.so') - run(executable, '--release-check', library) + run(executable, '--release-check', library, env=environment) manifest = {str(file.relative_to(STAGE)): hashlib.sha256(file.read_bytes()).hexdigest() for file in STAGE.rglob('*') if file.is_file()} (notices / 'SHA256SUMS.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8') diff --git a/tools/release/lock.py b/tools/release/lock.py new file mode 100644 index 0000000..0089c42 --- /dev/null +++ b/tools/release/lock.py @@ -0,0 +1,30 @@ +"""Regenerates the release CI's hash-locked Python requirements with uv (https://docs.astral.sh/uv/). + + python tools/release/lock.py # keep the pins, follow edits to the inputs + python tools/release/lock.py --upgrade # newest allowed versions of everything + python tools/release/lock.py --upgrade-package numpy # one package + +One universal file serves CPython 3.11 on Windows, Linux and macOS. torch is the CPU build from +download.pytorch.org (macOS gets the regular wheel), so pip needs that index as an extra index (release.yml). +""" +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +OPTIONS = ['--python-version', '3.11', '--universal', '--fork-strategy', 'fewest', '--torch-backend', 'cpu', + '--generate-hashes', '--custom-compile-command', 'python tools/release/lock.py'] + + +def compile_lock(*args, **kwargs): + subprocess.run(['uv', 'pip', 'compile', '--quiet', *args, *OPTIONS, *sys.argv[1:]], cwd=ROOT, check=True, **kwargs) + + +def main(): + compile_lock('requirements-runtime.txt', 'tools/release/requirements-build.in', '-o', 'tools/release/requirements-lock.txt') + # Wake enrollment uses only EfficientWord-Net's ONNX module; its declared dependencies (PyAudio, twine, typer) + # are not installed (see install_reqs.sh), so it has its own --no-deps lock. + compile_lock('-', '--no-deps', '-o', 'tools/release/requirements-lock-no-deps.txt', input=b'EfficientWord-Net\n') + + +if __name__ == '__main__': main() diff --git a/tools/release/requirements-build.in b/tools/release/requirements-build.in new file mode 100644 index 0000000..0f32a74 --- /dev/null +++ b/tools/release/requirements-build.in @@ -0,0 +1,5 @@ +# Release CI tools, hash-locked together with requirements-runtime.txt by tools/release/lock.py. +# PyInstaller < 6.14.1 omits numpy._core._exceptions, which numpy >= 2.3 imports only from C. +pyinstaller>=6.14.1,<7 +pyinstaller-hooks-contrib>=2026.7 +pytest>=8.0 diff --git a/tools/release/requirements-lock-no-deps.txt b/tools/release/requirements-lock-no-deps.txt new file mode 100644 index 0000000..7c0b591 --- /dev/null +++ b/tools/release/requirements-lock-no-deps.txt @@ -0,0 +1,5 @@ +# This file was autogenerated by uv via the following command: +# python tools/release/lock.py +efficientword-net==1.0.5 \ + --hash=sha256:4118005275a6fd1ca8b2683a4ee7ce15220099f1d17a088ca37ce041de2cd421 \ + --hash=sha256:9094b1ac4f4d4ccb54069b15e87639e38f3c06443be383acccedc5c26055aa58 diff --git a/tools/release/requirements-lock.txt b/tools/release/requirements-lock.txt new file mode 100644 index 0000000..1c1ea4e --- /dev/null +++ b/tools/release/requirements-lock.txt @@ -0,0 +1,2661 @@ +# This file was autogenerated by uv via the following command: +# python tools/release/lock.py +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 + # via aiohttp +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # -r requirements-runtime.txt + # discord-py +aiosignal==1.4.0 \ + --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ + --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 + # via aiohttp +altgraph==0.17.5 \ + --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ + --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 + # via + # macholib + # pyinstaller +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + # via pydantic +anyio==4.15.1 \ + --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \ + --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94 + # via + # httpx + # httpx2 + # openai + # starlette +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via aiohttp +audioop-lts==0.2.2 ; python_full_version >= '3.13' \ + --hash=sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a \ + --hash=sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449 \ + --hash=sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3 \ + --hash=sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0 \ + --hash=sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7 \ + --hash=sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58 \ + --hash=sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911 \ + --hash=sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7 \ + --hash=sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e \ + --hash=sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd \ + --hash=sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636 \ + --hash=sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623 \ + --hash=sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b \ + --hash=sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5 \ + --hash=sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a \ + --hash=sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a \ + --hash=sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6 \ + --hash=sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d \ + --hash=sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303 \ + --hash=sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602 \ + --hash=sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b \ + --hash=sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f \ + --hash=sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0 \ + --hash=sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09 \ + --hash=sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132 \ + --hash=sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753 \ + --hash=sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9 \ + --hash=sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547 \ + --hash=sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c \ + --hash=sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75 \ + --hash=sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e \ + --hash=sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093 \ + --hash=sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b \ + --hash=sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6 \ + --hash=sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd \ + --hash=sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8 \ + --hash=sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb \ + --hash=sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969 \ + --hash=sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7 \ + --hash=sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe \ + --hash=sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917 \ + --hash=sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc \ + --hash=sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9 \ + --hash=sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3 \ + --hash=sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5 \ + --hash=sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf \ + --hash=sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6 \ + --hash=sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19 \ + --hash=sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800 + # via discord-py +av==18.1.0 \ + --hash=sha256:025f84494cb23278498f03b0d8117d3e47a1cbc9c44b97eb31875cf02251e46b \ + --hash=sha256:08a9ae288299cfcbf739dba4ad0c53b9b71f45184303dd45947920d022fed695 \ + --hash=sha256:191224788d87af06c31784a395bb73f14b72f33d7f4871ace0157de2abdc6276 \ + --hash=sha256:3c8b1f8b46f99d52e2d8b0ed5d0cdadf172d24794d46e2077b16e44ed08e26ff \ + --hash=sha256:47bfc286e1bc9de7ab4681fc2b575cd2460a66919d31ffe1bd5aa54fae531a28 \ + --hash=sha256:5509ec12aaa19fd6601de13cfa6f4cdad450da07982118510592875d970454d6 \ + --hash=sha256:5548b79e2bf1f59b3e9aedc918a72d9dc45b9adaac10ff9470d5dbdda0002e47 \ + --hash=sha256:5cd9085028902c9880622bd37a12fd4b33060f06a52311f6f4867ca9f29a2c3b \ + --hash=sha256:6fc837cc51adf80331ac850779cd53b5d4c4460b0ebe9057a02a921c6736f19d \ + --hash=sha256:8a032e8d8ebc73dec079364b9b4a6837638a2d106e8472314e685ffbf163e700 \ + --hash=sha256:ab5ac081bc9eaf54109120d4e56284674fecfbe520d9aa1707c7fa911ec5f4d2 \ + --hash=sha256:ae75d8bb6467895ed1f8572ededf7ffa49eac07f6e483222f5d7d62a41d12f04 \ + --hash=sha256:b30a4e8d934558e19602b68998a4d9ac9f250fa0dacef216f7e8e40153b13316 \ + --hash=sha256:b36b0bae9e4c62f9487c99481ec15e4e3870fcc868522cd6d18fc2d6bfa04f01 \ + --hash=sha256:cf8a17466bef07765dbdecc9e66ed9b25d20b4e14f654fbf35345a58ac45fa0c \ + --hash=sha256:d49a5c542dfdc00f43c6cdb6cc41dac1781ee206fe180b56aa7433dfa816dfae \ + --hash=sha256:e4d48b9f12cad009cc72fe4f4099107de5e819c95f82767f4fd01a01481c0661 \ + --hash=sha256:e7ea063f6690193ea335a1d592d6e0274350d45e2ed6af83ee107cb90cbfd84f \ + --hash=sha256:ea1480b7a8d5405cb5f382b344731bf125fd2c1c6fae3964f6c48595628387ff + # via faster-whisper +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via + # sounddevice + # soundfile +charset-normalizer==3.5.2 \ + --hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \ + --hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \ + --hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \ + --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \ + --hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \ + --hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \ + --hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \ + --hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \ + --hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \ + --hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \ + --hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \ + --hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \ + --hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \ + --hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \ + --hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \ + --hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \ + --hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \ + --hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \ + --hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \ + --hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \ + --hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \ + --hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \ + --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \ + --hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \ + --hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \ + --hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \ + --hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \ + --hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \ + --hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \ + --hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \ + --hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \ + --hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \ + --hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \ + --hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \ + --hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \ + --hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \ + --hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \ + --hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \ + --hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \ + --hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \ + --hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \ + --hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \ + --hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \ + --hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \ + --hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \ + --hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \ + --hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \ + --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \ + --hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \ + --hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \ + --hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \ + --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \ + --hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \ + --hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \ + --hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \ + --hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \ + --hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \ + --hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \ + --hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \ + --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \ + --hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \ + --hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \ + --hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \ + --hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \ + --hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \ + --hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \ + --hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \ + --hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \ + --hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \ + --hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \ + --hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \ + --hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \ + --hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \ + --hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \ + --hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \ + --hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \ + --hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \ + --hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \ + --hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \ + --hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \ + --hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \ + --hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \ + --hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \ + --hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \ + --hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \ + --hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \ + --hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \ + --hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \ + --hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \ + --hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \ + --hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \ + --hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \ + --hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \ + --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \ + --hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \ + --hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \ + --hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \ + --hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \ + --hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \ + --hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \ + --hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \ + --hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \ + --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \ + --hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \ + --hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \ + --hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \ + --hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \ + --hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \ + --hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \ + --hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \ + --hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \ + --hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \ + --hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \ + --hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \ + --hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \ + --hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \ + --hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \ + --hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \ + --hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \ + --hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \ + --hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \ + --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \ + --hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \ + --hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \ + --hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \ + --hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \ + --hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \ + --hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \ + --hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \ + --hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \ + --hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \ + --hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \ + --hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \ + --hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \ + --hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \ + --hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \ + --hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \ + --hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \ + --hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \ + --hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \ + --hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \ + --hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \ + --hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \ + --hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \ + --hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \ + --hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \ + --hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \ + --hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \ + --hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \ + --hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \ + --hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \ + --hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \ + --hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \ + --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \ + --hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \ + --hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \ + --hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \ + --hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \ + --hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \ + --hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \ + --hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \ + --hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \ + --hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \ + --hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \ + --hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \ + --hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \ + --hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \ + --hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \ + --hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \ + --hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \ + --hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \ + --hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f + # via requests +click==8.5.0 \ + --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ + --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 + # via + # huggingface-hub + # uvicorn +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via joblib +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # pytest + # tqdm + # typer +ctranslate2==4.8.2 \ + --hash=sha256:00bc9f44172d05bd2becefe757d2397ab3292fad1695169f9ebb9c16eb497ce2 \ + --hash=sha256:077e96d0c9c7308b989538b6f9955e9c42e87b65cb4c7005817f938ba497b6d4 \ + --hash=sha256:0e55a43e68762bcac3529aa469ea6d2af0d35b84f9187212a8a1079acc9b00f5 \ + --hash=sha256:1e866ccf5f5668f7302c39b535d19d7e5811f3f00d6246faf6ed6fa070fa1af3 \ + --hash=sha256:249b35f575adf8dd11e6ba43b212d5cbfc845d1140abc610c2872eef1ffb3481 \ + --hash=sha256:2778eaf89340062feb0d2d3dd35bedb14644673ccbce8934782dc8d093b2cab9 \ + --hash=sha256:2d86e5c26e3fc0a1dce410951fecbce4f92e3facbd1a62c93d92276bcbd53d08 \ + --hash=sha256:32b9d0e984aa17a0da9b37bb7e4dcc638e898a2c32de08d5a4db53f15f640faa \ + --hash=sha256:3823c9883c2c410a76b2f19feda9da628a3c112cedfd912db815e0055c8235e2 \ + --hash=sha256:399c20a7336b6358f69ce3c615e090eabc08f29735a079fd1ac1e464119e0861 \ + --hash=sha256:3b40592b45c4116e651321a0371c48cbebe22d856159b896afe7728bf694a9a6 \ + --hash=sha256:43adddac06f387b12ec921bcdd8fb50525eae4f43672fe64e15ebeea9854b040 \ + --hash=sha256:5385f15493e7b6f41377da83d0d5afeb8e0f36188260722f3800c3c7121455af \ + --hash=sha256:554a0b2abf098a11c66ef7930c0a05b3de683a455e8ea3f2e6ab6966aaabc136 \ + --hash=sha256:573dcf5d96034dd9bcf7ee0db1657af490a52292ffc77dfb4b104da1e93322ad \ + --hash=sha256:58b1a78d050a7f281907b8acfe6f0cdbbf2feaf7464f38eb05b846f83fbaa297 \ + --hash=sha256:633f19d1ff8d053a8b179ca742b242002ac7882a2ab44e319c6efcde27d586a4 \ + --hash=sha256:70e2388d31dfb59968e6e011d01a6fc6bf802c0e1ad920a8d2a413f1c9da02ae \ + --hash=sha256:781f835da7fdc4adcc38f9d674dbe9b37eaf3c9aee3b2bc17d684342ff59d549 \ + --hash=sha256:8221875b09ef982e3579a5165f8c9fcfb3d5de0f710af228eca2f73d69635ace \ + --hash=sha256:8ad4ee86fb93e8d7456fbcf77735d33c51eda4f363e15db4151b0e56e8633677 \ + --hash=sha256:8f87f29d4b1c26ec7437f147d70cc52a53465acfcd2e754d3a59c12fdb932ae7 \ + --hash=sha256:94f4a0c2a9641b18a2d1b873ad615efe95a5417a88f63470f3ca11088c9754df \ + --hash=sha256:980f13e1d41987908af945861081158fbcb2983f4f9399514adc8c87378b5587 \ + --hash=sha256:995938fcd24a1174a7abf9765e7fa216b5b91a1d8e8c4c8f383c7a186e8bab2e \ + --hash=sha256:9a732c7d58b89608c28cc4f04761c76a17e9fdab443e78c6857c736599bc8d72 \ + --hash=sha256:a24e0a95151b970941867fec94c983df978630f1996242a587c072d455607d28 \ + --hash=sha256:a40b190248389e1adb38ff90407937131c6bb9d7f9771f29ca1dca01946ff252 \ + --hash=sha256:cb2a5b4c206b3bd1f7f4b44d07e86e42050af6753053cdc86444f8d0ead9d6d8 \ + --hash=sha256:d94421d565d0de61c032998f737a18942b0f2bef40c0424b1846ec6f67300105 \ + --hash=sha256:dd9ab8b521230e8e962a58362b1ccde582806e781af8c1b9d7b244f3024468b1 \ + --hash=sha256:e32ae5e625a8999749745de43c2de5c12ceebb6d0d9cc9a3a956e8912bd68edb \ + --hash=sha256:e563497eafeebb6417678d4fc948edb7995d02cb8d1ef479bdabc4496edd7558 \ + --hash=sha256:f6f0b576c247984d3fc299a372ccc9319b668d6d25b0539f3c861beba15d0504 \ + --hash=sha256:fedda669421a57f8164568ee86ea265727016988d310fcc2e24ca30e9ec457cd + # via faster-whisper +discord-py==2.7.1 \ + --hash=sha256:24d5e6a45535152e4b98148a9dd6b550d25dc2c9fb41b6d670319411641249da \ + --hash=sha256:849dca2c63b171146f3a7f3f8acc04248098e9e6203412ce3cf2745f284f7439 + # via -r requirements-runtime.txt +fastapi==0.142.2 \ + --hash=sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570 \ + --hash=sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b + # via -r requirements-runtime.txt +faster-whisper==1.2.1 \ + --hash=sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7 + # via -r requirements-runtime.txt +filelock==4.0.10 \ + --hash=sha256:00d6a81f976a6332551c2c10f39e12b4abb7e01c64d4c497b81a615bc9186f1f \ + --hash=sha256:698048d158dc3c9e18cc5e88b3efdd4bd0e77d76686b22adb4511cc4b2c2f0db + # via + # huggingface-hub + # torch +flatbuffers==25.12.19 \ + --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 + # via onnxruntime +frozenlist==1.8.0 \ + --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ + --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ + --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ + --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ + --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ + --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ + --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ + --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ + --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ + --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ + --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ + --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ + --hash=sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4 \ + --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ + --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ + --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \ + --hash=sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3 \ + --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ + --hash=sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087 \ + --hash=sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068 \ + --hash=sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7 \ + --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ + --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ + --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ + --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \ + --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ + --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ + --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ + --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ + --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ + --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ + --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ + --hash=sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675 \ + --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ + --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ + --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ + --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ + --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ + --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ + --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ + --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ + --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ + --hash=sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c \ + --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ + --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ + --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ + --hash=sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5 \ + --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ + --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ + --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ + --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ + --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ + --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ + --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ + --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ + --hash=sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95 \ + --hash=sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1 \ + --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ + --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ + --hash=sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6 \ + --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ + --hash=sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459 \ + --hash=sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a \ + --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ + --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ + --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ + --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ + --hash=sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186 \ + --hash=sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6 \ + --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ + --hash=sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e \ + --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ + --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ + --hash=sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450 \ + --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ + --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ + --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ + --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ + --hash=sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178 \ + --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ + --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ + --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ + --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ + --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ + --hash=sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61 \ + --hash=sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca \ + --hash=sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad \ + --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \ + --hash=sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a \ + --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ + --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ + --hash=sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011 \ + --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ + --hash=sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103 \ + --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ + --hash=sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda \ + --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ + --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ + --hash=sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e \ + --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ + --hash=sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef \ + --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ + --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ + --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ + --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ + --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ + --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ + --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ + --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ + --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \ + --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ + --hash=sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47 \ + --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ + --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ + --hash=sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f \ + --hash=sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff \ + --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ + --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ + --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ + --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ + --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ + --hash=sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 \ + --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ + --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ + --hash=sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 \ + --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ + --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ + --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ + --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ + --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd + # via + # aiohttp + # aiosignal +fsspec==2026.9.0 \ + --hash=sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe \ + --hash=sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f + # via + # huggingface-hub + # torch +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via + # httpcore + # httpcore2 + # uvicorn +hf-xet==1.6.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' \ + --hash=sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7 \ + --hash=sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675 \ + --hash=sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef \ + --hash=sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9 \ + --hash=sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3 \ + --hash=sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb \ + --hash=sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e \ + --hash=sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338 \ + --hash=sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d \ + --hash=sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d \ + --hash=sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765 \ + --hash=sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c \ + --hash=sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522 \ + --hash=sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f \ + --hash=sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a \ + --hash=sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b \ + --hash=sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b + # via huggingface-hub +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpcore2==2.13.1 ; sys_platform != 'emscripten' \ + --hash=sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103 \ + --hash=sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a + # via httpx2 +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via + # -r requirements-runtime.txt + # huggingface-hub +httpx2==2.13.1 \ + --hash=sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4 \ + --hash=sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa + # via openai +httpx2-jsfetch==1.0 ; python_full_version >= '3.12' and sys_platform == 'emscripten' \ + --hash=sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60 \ + --hash=sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32 + # via httpx2 +huggingface-hub==1.33.0 \ + --hash=sha256:04e434b06e100eddbce9a6e817d72693a7884b10a79bd67ab48080d5c07eb899 \ + --hash=sha256:367be21a201db9523eddf8aeac7048f2602c1b308691c97640d5e72ed188007e + # via + # -r requirements-runtime.txt + # faster-whisper + # sentence-transformers + # tokenizers + # transformers +idna==3.20 \ + --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \ + --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c + # via + # anyio + # httpx + # httpx2 + # requests + # yarl +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via torch +jiter==0.17.0 \ + --hash=sha256:00b5a98df3e3a3e8cf7b619f4ac2f8bf975bbf3d95d02c5d17b8dbfe5c8b8245 \ + --hash=sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5 \ + --hash=sha256:0239520085cac678e77a606fd7e3f1c60c371d719790c5e3807388d3da4354c2 \ + --hash=sha256:02a360707033d8cef53f7f3480817a1489177a259ec6ec01e98c37e0b922ddca \ + --hash=sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9 \ + --hash=sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12 \ + --hash=sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76 \ + --hash=sha256:073dc68c1a700c8fc480e877864a6b6ffc887533e261f4380c08c16bf09d057a \ + --hash=sha256:0b52d52035b3907c5b1f6277857b29c1cbfc965e24e0f27330dbed83edb591ec \ + --hash=sha256:10c5349312e5cb02b7a21e123a57665afa895953f05bf252a9dd4c13a572b7ab \ + --hash=sha256:10cd64a5720ad7f809ac5466ff1705813f1b6b510f195a73acafba0ac0e1f675 \ + --hash=sha256:10f5558eed511b830488003449d942bd75829ad6257dc58cb9a03e596a7777b1 \ + --hash=sha256:11902505d401691720f5785c15b02204248526edee11b635cd6c40cd52b81599 \ + --hash=sha256:155be7355bdb7ca76ab0961be8982c225f964a5c073a83984183f22391cc29fc \ + --hash=sha256:16dd0c1baf098ae70b8f3616574eb3fedf34e26670b89e16a7e67561f737ed2d \ + --hash=sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa \ + --hash=sha256:29f49b325e0234e4ad9ecca5b861ffbd09b95ccac9bd46fa55841b6e56eea5fe \ + --hash=sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7 \ + --hash=sha256:300ce01ab0215e3dea4d00090143c909aedc65c0f809b3c07983e1d038f291b9 \ + --hash=sha256:30793a24a31e968969757c9e08d830cbb15a2cd3c4959b4498b38f4b1c2258eb \ + --hash=sha256:30c692d567ba206c7cca38c9d1d0ccc70c9786290173c184d871ca12e9981ed7 \ + --hash=sha256:32aaaa764604496610a3ad2d98503ae88ccb2fbe769e892ff4533e778e85f708 \ + --hash=sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd \ + --hash=sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8 \ + --hash=sha256:370d8fe5bf201dc6925e8a84c81ac7291f74d9fd1778234fc79d517064a5c76b \ + --hash=sha256:37150a9e02e869475854fa20b7d0d5e26d18d0f8bc17293999973ff27e99ae7a \ + --hash=sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71 \ + --hash=sha256:3ad556afc289f15d2b181b941982d01f06190863c07440185b9f354e1bd2def3 \ + --hash=sha256:3bf4dc2b84a464117fb097d15a25c58d100d2692888e3b0d92df5b48ed16b7c0 \ + --hash=sha256:3c1a5336c04a41b1f1cf9572e294aec27cc569767ff73de7bf87a91f0bea7cb9 \ + --hash=sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5 \ + --hash=sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2 \ + --hash=sha256:42b0260445251b1bc520a63baa94a32d88e0f931fba234f1764db7feb7c72174 \ + --hash=sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c \ + --hash=sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b \ + --hash=sha256:4607ec7d93355fbc25b8dc5189153cf21d66063b9f9cd04dd2774e6e783f9b6a \ + --hash=sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44 \ + --hash=sha256:492f37230bbf9581ab2c17bcda862c249afb9ae2e3ab2dd6db59943bc4cc3153 \ + --hash=sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc \ + --hash=sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b \ + --hash=sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a \ + --hash=sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2 \ + --hash=sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb \ + --hash=sha256:545c36a0f3b2238c242cc9785439d3242a871b7bc39fe3f441bcaa07bf3aa83e \ + --hash=sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8 \ + --hash=sha256:5888fe5abc1ca2fa834a3e1b4c7ef0dcece286a7d7e95a609ef0934b777b9fc9 \ + --hash=sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5 \ + --hash=sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0 \ + --hash=sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199 \ + --hash=sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718 \ + --hash=sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807 \ + --hash=sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4 \ + --hash=sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744 \ + --hash=sha256:64846211a2debe7c071d2146d2283d2b0c1c93dc8fd5fb7794faac2ca6061b5c \ + --hash=sha256:686c93d86f2b426c803024b805bd161a6cd10e9627c23e901640eab646c0ad8a \ + --hash=sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b \ + --hash=sha256:6af5b74073bd25bae695e6d00919f6a9be7ed5a9f8836d981eb1ffe84139e6fb \ + --hash=sha256:6b303d88e6a0bda789ec4b7801c7bad68e27230ba1fe4baffc756d1fbd32dc9d \ + --hash=sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388 \ + --hash=sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef \ + --hash=sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31 \ + --hash=sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03 \ + --hash=sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8 \ + --hash=sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63 \ + --hash=sha256:746243a080b4ca790b8499af3d7cf9825d5f5987933950cd818e767ee353d826 \ + --hash=sha256:755079792868ce5d4938e83b91a0939b34fb858a1ca65a104f2d771bea57faa1 \ + --hash=sha256:7573e80232c5bcf80c24c038cf7e53a463f5c3b1dd1dd4109d66304f4dccc233 \ + --hash=sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1 \ + --hash=sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9 \ + --hash=sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b \ + --hash=sha256:7b68d3495d95da120651a5628c7ebadee84ed001a1b76e6afc325c42482f15b5 \ + --hash=sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1 \ + --hash=sha256:81c83c0abe614446a283d994d2c07c4f58632dea2cdf66ba9e2921bb8ccd593e \ + --hash=sha256:826871c42cebaae22f0a2b5673a4a1a75c851bb2d13b3c17764a630a6b298984 \ + --hash=sha256:84963d3f395ef5e9a32ce47155e08a7962fa292c159a10cb98b931cef1416925 \ + --hash=sha256:84ac78df457e1ee3f7e733bd114823302ae8c5ad5542d7e6647d92ffaa090a04 \ + --hash=sha256:86d703d9faa1ffc8ae4e9de0fa007712ed2171b5c0d93811a8e2e105ac729b0d \ + --hash=sha256:86f3f9343a288eb85a81ef20a752b2f84564296636db54a9fff0b5c8deaf1df2 \ + --hash=sha256:8adca2e793288e5f1bb29279bb439d0d3cfbb50eddca7e7e6ffd42ff4f482406 \ + --hash=sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797 \ + --hash=sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9 \ + --hash=sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06 \ + --hash=sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f \ + --hash=sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac \ + --hash=sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3 \ + --hash=sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca \ + --hash=sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd \ + --hash=sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312 \ + --hash=sha256:af2f7501580f274b63c4b2283bc425f5df7edf06ae5b171e5f87d912ff359a20 \ + --hash=sha256:b550585523339b71cb852b811aae49d08d7601ad8ffe9f5dc1562f4c3d22fd87 \ + --hash=sha256:b75f85660108965a94be77911a25a253429307294d9415b3c597118977a614de \ + --hash=sha256:b847b18d066c46b3b7ae49d6c94a7634c5e4a8983146ee25562a092000f5e3ad \ + --hash=sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8 \ + --hash=sha256:c19b9357309b8cc6de8a48fca8e44a8c9c2feaaa2f5896d037fa505d48fcab80 \ + --hash=sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9 \ + --hash=sha256:cfafd7be8b16ceadd298db542cead37cddc211c4c49e04ad2596924df18625b1 \ + --hash=sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39 \ + --hash=sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff \ + --hash=sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23 \ + --hash=sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a \ + --hash=sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59 \ + --hash=sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165 \ + --hash=sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64 \ + --hash=sha256:e088612ff90ebc9247e1a43074b72835804261c47e6a6c01cb3ddcb55360d688 \ + --hash=sha256:e654b6b04e39c9cb19cb8b04c6ddf1f2db07751fa14156413969fd78bad0e5cb \ + --hash=sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33 \ + --hash=sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768 \ + --hash=sha256:eb2295da7c3769f6719b227a237aa6a5cfa6550e478bc838001b592c57e16575 \ + --hash=sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91 \ + --hash=sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3 \ + --hash=sha256:ed1a24005daac667d577402d75a2922f9775a165b146b883ff1ad3602d8be689 \ + --hash=sha256:efe9f61bb30174d2f5c8396445c360c96c44e78164d0815dfe627ccf57849574 \ + --hash=sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a \ + --hash=sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812 \ + --hash=sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478 \ + --hash=sha256:fa13acf1046f95df808c64b1310705e143fab87aee73ae00cc42d640867fd2c1 \ + --hash=sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489 \ + --hash=sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8 \ + --hash=sha256:ffa0380ad091de7d3fc33e17a97ff479851ee18a0a2a3ee56ff3215cdc886656 + # via openai +joblib==1.6.0 \ + --hash=sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03 \ + --hash=sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba + # via scikit-learn +macholib==1.16.4 ; sys_platform == 'darwin' \ + --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ + --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 + # via pyinstaller +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +markupsafe==3.0.4 \ + --hash=sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98 \ + --hash=sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002 \ + --hash=sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b \ + --hash=sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653 \ + --hash=sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c \ + --hash=sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e \ + --hash=sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc \ + --hash=sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a \ + --hash=sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92 \ + --hash=sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f \ + --hash=sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97 \ + --hash=sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4 \ + --hash=sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7 \ + --hash=sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691 \ + --hash=sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2 \ + --hash=sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc \ + --hash=sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde \ + --hash=sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99 \ + --hash=sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9 \ + --hash=sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df \ + --hash=sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5 \ + --hash=sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17 \ + --hash=sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8 \ + --hash=sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc \ + --hash=sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b \ + --hash=sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea \ + --hash=sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248 \ + --hash=sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741 \ + --hash=sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5 \ + --hash=sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6 \ + --hash=sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7 \ + --hash=sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1 \ + --hash=sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67 \ + --hash=sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f \ + --hash=sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9 \ + --hash=sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c \ + --hash=sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc \ + --hash=sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba \ + --hash=sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17 \ + --hash=sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf \ + --hash=sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6 \ + --hash=sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2 \ + --hash=sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163 \ + --hash=sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278 \ + --hash=sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d \ + --hash=sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b \ + --hash=sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634 \ + --hash=sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38 \ + --hash=sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed \ + --hash=sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c \ + --hash=sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148 \ + --hash=sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a \ + --hash=sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7 \ + --hash=sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f \ + --hash=sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811 \ + --hash=sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e \ + --hash=sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295 \ + --hash=sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2 \ + --hash=sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7 \ + --hash=sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0 \ + --hash=sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6 \ + --hash=sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed \ + --hash=sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378 \ + --hash=sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0 \ + --hash=sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac \ + --hash=sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b \ + --hash=sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96 \ + --hash=sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59 \ + --hash=sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808 \ + --hash=sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2 \ + --hash=sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb \ + --hash=sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65 \ + --hash=sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72 \ + --hash=sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8 \ + --hash=sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e \ + --hash=sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91 \ + --hash=sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a \ + --hash=sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2 \ + --hash=sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e \ + --hash=sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707 \ + --hash=sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21 \ + --hash=sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef \ + --hash=sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be \ + --hash=sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453 \ + --hash=sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a \ + --hash=sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6 \ + --hash=sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977 \ + --hash=sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978 \ + --hash=sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581 \ + --hash=sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692 \ + --hash=sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3 \ + --hash=sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369 \ + --hash=sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a \ + --hash=sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36 \ + --hash=sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9 \ + --hash=sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768 \ + --hash=sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916 \ + --hash=sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b \ + --hash=sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f \ + --hash=sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346 \ + --hash=sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c \ + --hash=sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464 \ + --hash=sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9 \ + --hash=sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee \ + --hash=sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300 \ + --hash=sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6 \ + --hash=sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d \ + --hash=sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868 \ + --hash=sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46 \ + --hash=sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97 \ + --hash=sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733 \ + --hash=sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe \ + --hash=sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16 \ + --hash=sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429 \ + --hash=sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39 \ + --hash=sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894 \ + --hash=sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c \ + --hash=sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c \ + --hash=sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169 \ + --hash=sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa \ + --hash=sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77 \ + --hash=sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe \ + --hash=sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad \ + --hash=sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85 \ + --hash=sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e \ + --hash=sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34 \ + --hash=sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a \ + --hash=sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9 \ + --hash=sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c \ + --hash=sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749 \ + --hash=sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214 \ + --hash=sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932 \ + --hash=sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494 \ + --hash=sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889 \ + --hash=sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1 \ + --hash=sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0 \ + --hash=sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2 \ + --hash=sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786 \ + --hash=sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78 \ + --hash=sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e \ + --hash=sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8 \ + --hash=sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289 \ + --hash=sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c \ + --hash=sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe \ + --hash=sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237 \ + --hash=sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd \ + --hash=sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624 \ + --hash=sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19 \ + --hash=sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977 \ + --hash=sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8 \ + --hash=sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3 + # via jinja2 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + # via sympy +multidict==6.9.1 \ + --hash=sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c \ + --hash=sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9 \ + --hash=sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa \ + --hash=sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188 \ + --hash=sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201 \ + --hash=sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382 \ + --hash=sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f \ + --hash=sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919 \ + --hash=sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418 \ + --hash=sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771 \ + --hash=sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c \ + --hash=sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76 \ + --hash=sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345 \ + --hash=sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2 \ + --hash=sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580 \ + --hash=sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9 \ + --hash=sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1 \ + --hash=sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1 \ + --hash=sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64 \ + --hash=sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5 \ + --hash=sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006 \ + --hash=sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8 \ + --hash=sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02 \ + --hash=sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132 \ + --hash=sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c \ + --hash=sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19 \ + --hash=sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b \ + --hash=sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba \ + --hash=sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b \ + --hash=sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08 \ + --hash=sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe \ + --hash=sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f \ + --hash=sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd \ + --hash=sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879 \ + --hash=sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f \ + --hash=sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9 \ + --hash=sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987 \ + --hash=sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd \ + --hash=sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea \ + --hash=sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc \ + --hash=sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c \ + --hash=sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b \ + --hash=sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709 \ + --hash=sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4 \ + --hash=sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce \ + --hash=sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94 \ + --hash=sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a \ + --hash=sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e \ + --hash=sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b \ + --hash=sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96 \ + --hash=sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1 \ + --hash=sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e \ + --hash=sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455 \ + --hash=sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3 \ + --hash=sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471 \ + --hash=sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d \ + --hash=sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b \ + --hash=sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d \ + --hash=sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f \ + --hash=sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004 \ + --hash=sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4 \ + --hash=sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e \ + --hash=sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5 \ + --hash=sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60 \ + --hash=sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14 \ + --hash=sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f \ + --hash=sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9 \ + --hash=sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3 \ + --hash=sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194 \ + --hash=sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a \ + --hash=sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3 \ + --hash=sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42 \ + --hash=sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c \ + --hash=sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828 \ + --hash=sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290 \ + --hash=sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485 \ + --hash=sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e \ + --hash=sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8 \ + --hash=sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c \ + --hash=sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b \ + --hash=sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659 \ + --hash=sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b \ + --hash=sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9 \ + --hash=sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953 \ + --hash=sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c \ + --hash=sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1 \ + --hash=sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299 \ + --hash=sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94 \ + --hash=sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983 \ + --hash=sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d \ + --hash=sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac \ + --hash=sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a \ + --hash=sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7 \ + --hash=sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e \ + --hash=sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45 \ + --hash=sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410 \ + --hash=sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40 \ + --hash=sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4 \ + --hash=sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805 \ + --hash=sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d \ + --hash=sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa \ + --hash=sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d \ + --hash=sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701 \ + --hash=sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80 \ + --hash=sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6 \ + --hash=sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40 \ + --hash=sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20 \ + --hash=sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694 \ + --hash=sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a \ + --hash=sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203 \ + --hash=sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519 \ + --hash=sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125 \ + --hash=sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0 \ + --hash=sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d \ + --hash=sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a \ + --hash=sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea \ + --hash=sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2 \ + --hash=sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c \ + --hash=sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3 \ + --hash=sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d \ + --hash=sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3 \ + --hash=sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a \ + --hash=sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68 \ + --hash=sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8 \ + --hash=sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca \ + --hash=sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8 \ + --hash=sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840 \ + --hash=sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82 \ + --hash=sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610 \ + --hash=sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca \ + --hash=sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a \ + --hash=sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1 \ + --hash=sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5 \ + --hash=sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a \ + --hash=sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13 \ + --hash=sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07 \ + --hash=sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05 \ + --hash=sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1 \ + --hash=sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4 \ + --hash=sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2 \ + --hash=sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82 \ + --hash=sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6 \ + --hash=sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d \ + --hash=sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88 \ + --hash=sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f \ + --hash=sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126 \ + --hash=sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24 \ + --hash=sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab \ + --hash=sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67 \ + --hash=sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3 \ + --hash=sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1 \ + --hash=sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800 \ + --hash=sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3 \ + --hash=sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8 \ + --hash=sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5 \ + --hash=sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24 \ + --hash=sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c \ + --hash=sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a \ + --hash=sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841 \ + --hash=sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387 \ + --hash=sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec \ + --hash=sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721 \ + --hash=sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d \ + --hash=sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390 \ + --hash=sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1 \ + --hash=sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d \ + --hash=sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e \ + --hash=sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df \ + --hash=sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb \ + --hash=sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5 \ + --hash=sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9 + # via + # aiohttp + # yarl +narwhals==2.26.0 \ + --hash=sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54 \ + --hash=sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd + # via scikit-learn +networkx==3.6.1 \ + --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \ + --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 + # via torch +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 + # via + # -r requirements-runtime.txt + # ctranslate2 + # onnxruntime + # scikit-learn + # scipy + # sentence-transformers + # soundfile + # transformers +onnxruntime==1.30.0 \ + --hash=sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd \ + --hash=sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134 \ + --hash=sha256:0edd0145a6e3fce8a1276491dc784d615e3c58bcb952c9b4e5c876d5c6a12ad7 \ + --hash=sha256:16506cfa5d218850f2b46e0edce1b952121f2f84f8e0074dafe15f4787f0c8fb \ + --hash=sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294 \ + --hash=sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4 \ + --hash=sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca \ + --hash=sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93 \ + --hash=sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9 \ + --hash=sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7 \ + --hash=sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83 \ + --hash=sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d \ + --hash=sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f \ + --hash=sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec \ + --hash=sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547 \ + --hash=sha256:bac3aee7b40acd805659fd81a2acd6096312d5b0f014c97ddfe75fb4c1644627 \ + --hash=sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f \ + --hash=sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175 \ + --hash=sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262 \ + --hash=sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e \ + --hash=sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38 \ + --hash=sha256:f36c87ec504f4f328e968ee322e4ba9682abe66280b3425625afcbed5333fed7 \ + --hash=sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328 \ + --hash=sha256:fd54b314ea385bcecac69ab431f020ba503e3878dad4ebb645fec5a24b041242 + # via + # -r requirements-runtime.txt + # faster-whisper +openai==3.24.0 \ + --hash=sha256:1e7463f7d78773ab2ce4fe85710481aa5bd5ffefd54c8de4b067506cd2d42895 \ + --hash=sha256:b59703a3e3b5df78b269e90049d383a1dda58c90104cc4be59db21078db4b7a4 + # via -r requirements-runtime.txt +opentelemetry-api==1.45.0 \ + --hash=sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7 \ + --hash=sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3 + # via fastapi +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # huggingface-hub + # onnxruntime + # pyinstaller + # pyinstaller-hooks-contrib + # pytest + # silero-vad + # transformers +pefile==2024.8.26 ; sys_platform == 'win32' \ + --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ + --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f + # via pyinstaller +pillow==12.3.0 \ + --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ + --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ + --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ + --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ + --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ + --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ + --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ + --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ + --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ + --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ + --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ + --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ + --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ + --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ + --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ + --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ + --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ + --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ + --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ + --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ + --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ + --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ + --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ + --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ + --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ + --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ + --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ + --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ + --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ + --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ + --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ + --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ + --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ + --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ + --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ + --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ + --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ + --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ + --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ + --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ + --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ + --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ + --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ + --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ + --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ + --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ + --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ + --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ + --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ + --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ + --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ + --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ + --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ + --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ + --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ + --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ + --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ + --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ + --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ + --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ + --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ + --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ + --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ + --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ + --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ + --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ + --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ + --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ + --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ + --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ + --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ + --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ + --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ + --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ + --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ + --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ + --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ + --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ + --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ + --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ + --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ + --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ + --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ + --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ + --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ + --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ + --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 + # via -r requirements-runtime.txt +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +propcache==0.5.4 \ + --hash=sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5 \ + --hash=sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432 \ + --hash=sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72 \ + --hash=sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97 \ + --hash=sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf \ + --hash=sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983 \ + --hash=sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77 \ + --hash=sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1 \ + --hash=sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c \ + --hash=sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284 \ + --hash=sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827 \ + --hash=sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b \ + --hash=sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8 \ + --hash=sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745 \ + --hash=sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae \ + --hash=sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb \ + --hash=sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320 \ + --hash=sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d \ + --hash=sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b \ + --hash=sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec \ + --hash=sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4 \ + --hash=sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c \ + --hash=sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc \ + --hash=sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a \ + --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b \ + --hash=sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9 \ + --hash=sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b \ + --hash=sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea \ + --hash=sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51 \ + --hash=sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831 \ + --hash=sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba \ + --hash=sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad \ + --hash=sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56 \ + --hash=sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f \ + --hash=sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25 \ + --hash=sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2 \ + --hash=sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d \ + --hash=sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d \ + --hash=sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d \ + --hash=sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd \ + --hash=sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a \ + --hash=sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c \ + --hash=sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111 \ + --hash=sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e \ + --hash=sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac \ + --hash=sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c \ + --hash=sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760 \ + --hash=sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57 \ + --hash=sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7 \ + --hash=sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993 \ + --hash=sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3 \ + --hash=sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3 \ + --hash=sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757 \ + --hash=sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc \ + --hash=sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51 \ + --hash=sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0 \ + --hash=sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838 \ + --hash=sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826 \ + --hash=sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8 \ + --hash=sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589 \ + --hash=sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef \ + --hash=sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8 \ + --hash=sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468 \ + --hash=sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177 \ + --hash=sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab \ + --hash=sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487 \ + --hash=sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5 \ + --hash=sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788 \ + --hash=sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece \ + --hash=sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792 \ + --hash=sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62 \ + --hash=sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d \ + --hash=sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f \ + --hash=sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47 \ + --hash=sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944 \ + --hash=sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21 \ + --hash=sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5 \ + --hash=sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737 \ + --hash=sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e \ + --hash=sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12 \ + --hash=sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161 \ + --hash=sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e \ + --hash=sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9 \ + --hash=sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9 \ + --hash=sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb \ + --hash=sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526 \ + --hash=sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d \ + --hash=sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc \ + --hash=sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef \ + --hash=sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9 \ + --hash=sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b \ + --hash=sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370 \ + --hash=sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162 \ + --hash=sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546 \ + --hash=sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1 \ + --hash=sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f \ + --hash=sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f \ + --hash=sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38 \ + --hash=sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894 \ + --hash=sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429 \ + --hash=sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8 \ + --hash=sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a \ + --hash=sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680 \ + --hash=sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d \ + --hash=sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c \ + --hash=sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574 \ + --hash=sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8 \ + --hash=sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867 \ + --hash=sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6 \ + --hash=sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30 \ + --hash=sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4 \ + --hash=sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9 \ + --hash=sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f \ + --hash=sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937 \ + --hash=sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3 \ + --hash=sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031 \ + --hash=sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39 \ + --hash=sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2 \ + --hash=sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e \ + --hash=sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9 \ + --hash=sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c \ + --hash=sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e \ + --hash=sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8 \ + --hash=sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36 \ + --hash=sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0 \ + --hash=sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565 \ + --hash=sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c \ + --hash=sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858 \ + --hash=sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5 \ + --hash=sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97 \ + --hash=sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123 \ + --hash=sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9 \ + --hash=sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874 \ + --hash=sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68 \ + --hash=sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2 \ + --hash=sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572 \ + --hash=sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a \ + --hash=sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558 + # via + # aiohttp + # yarl +protobuf==7.36.2 \ + --hash=sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb \ + --hash=sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2 \ + --hash=sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728 \ + --hash=sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353 \ + --hash=sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e \ + --hash=sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e \ + --hash=sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e \ + --hash=sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf + # via onnxruntime +pycparser==3.0 ; implementation_name != 'PyPy' \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi +pydantic==2.13.5 \ + --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 \ + --hash=sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08 + # via + # fastapi + # openai +pydantic-core==2.46.5 \ + --hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \ + --hash=sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821 \ + --hash=sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5 \ + --hash=sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c \ + --hash=sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184 \ + --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 \ + --hash=sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc \ + --hash=sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5 \ + --hash=sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0 \ + --hash=sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931 \ + --hash=sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e \ + --hash=sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25 \ + --hash=sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d \ + --hash=sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6 \ + --hash=sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47 \ + --hash=sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038 \ + --hash=sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8 \ + --hash=sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b \ + --hash=sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a \ + --hash=sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e \ + --hash=sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074 \ + --hash=sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0 \ + --hash=sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433 \ + --hash=sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f \ + --hash=sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034 \ + --hash=sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21 \ + --hash=sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736 \ + --hash=sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0 \ + --hash=sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7 \ + --hash=sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c \ + --hash=sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4 \ + --hash=sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c \ + --hash=sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c \ + --hash=sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069 \ + --hash=sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb \ + --hash=sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061 \ + --hash=sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29 \ + --hash=sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3 \ + --hash=sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa \ + --hash=sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e \ + --hash=sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38 \ + --hash=sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f \ + --hash=sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b \ + --hash=sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8 \ + --hash=sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e \ + --hash=sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c \ + --hash=sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be \ + --hash=sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6 \ + --hash=sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793 \ + --hash=sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1 \ + --hash=sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b \ + --hash=sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64 \ + --hash=sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f \ + --hash=sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761 \ + --hash=sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d \ + --hash=sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168 \ + --hash=sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869 \ + --hash=sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111 \ + --hash=sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5 \ + --hash=sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b \ + --hash=sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7 \ + --hash=sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129 \ + --hash=sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e \ + --hash=sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655 \ + --hash=sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c \ + --hash=sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec \ + --hash=sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689 \ + --hash=sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f \ + --hash=sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf \ + --hash=sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea \ + --hash=sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9 \ + --hash=sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464 \ + --hash=sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519 \ + --hash=sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b \ + --hash=sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f \ + --hash=sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee \ + --hash=sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a \ + --hash=sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e \ + --hash=sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6 \ + --hash=sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2 \ + --hash=sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f \ + --hash=sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a \ + --hash=sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f \ + --hash=sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a \ + --hash=sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47 \ + --hash=sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda \ + --hash=sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0 \ + --hash=sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4 \ + --hash=sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d \ + --hash=sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3 \ + --hash=sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2 \ + --hash=sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355 \ + --hash=sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461 \ + --hash=sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed \ + --hash=sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f \ + --hash=sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5 \ + --hash=sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2 \ + --hash=sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829 \ + --hash=sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0 \ + --hash=sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266 \ + --hash=sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575 \ + --hash=sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290 \ + --hash=sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f \ + --hash=sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62 \ + --hash=sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f \ + --hash=sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a \ + --hash=sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7 \ + --hash=sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed \ + --hash=sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f \ + --hash=sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1 \ + --hash=sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615 \ + --hash=sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8 \ + --hash=sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3 \ + --hash=sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a \ + --hash=sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff \ + --hash=sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084 \ + --hash=sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0 \ + --hash=sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3 \ + --hash=sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13 \ + --hash=sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9 + # via pydantic +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via + # pytest + # rich +pyinstaller==6.22.3 \ + --hash=sha256:020d4fe0627f5c73dfbcfad1a6578ef354d5fe4bdccd9d8e5a615528f5a45d2b \ + --hash=sha256:052f4a1cd4f81092ccb7a18fe8ebc9ecf832a5917317e39fb3775af4e959b253 \ + --hash=sha256:05eb2f5615503e72939a7224d68b4aff572c6b0438ee4a17d0a4b481f399362d \ + --hash=sha256:312da84b4b31ab9750066a823734c11b7c68757fdd67c9f038bf19c330a954e4 \ + --hash=sha256:31df3cc4261e804a53d358b7897c293ec8b88b167a7cca06c78018085c855f97 \ + --hash=sha256:451a4ae14b719365bf1a2f0a99dae7b3463060061c3a394c70d5264cfb439528 \ + --hash=sha256:46a3e30a118341a1ec20667a5a95aa7e0aa8c4fa129893c0151b3eb3f801a722 \ + --hash=sha256:500bd58c7bf7e584a8435adccbd763a0b918d5c12b08d74ff50fd79b2915458b \ + --hash=sha256:9e4f7bd64b2103f14b4a6240fb9155e9fe7bc61391b4043bbddb7d3ebc8ab0fa \ + --hash=sha256:a2b6fc26601e8c2596c3d54981566934a3382e85d05d0fe91b55b6a8dcc6b282 \ + --hash=sha256:ea240b5dfa831ff673c97505eee0ec24670bccf71396c83109440348944c83d0 \ + --hash=sha256:f82d0fb89f21c3ed6f482ee16f341bc59e8a31dbabb9bae9ef04e53ade4dc0d9 + # via -r tools/release/requirements-build.in +pyinstaller-hooks-contrib==2026.8 \ + --hash=sha256:4d825786ad7a9b7dbcc52d612748a34562fe273461bcfd88f4d549c594bbf8f9 \ + --hash=sha256:6d21d65323ea4467753afc21f52d7d0e69416ae8101dd88269bf273ad48db084 + # via + # -r tools/release/requirements-build.in + # pyinstaller +pypdf==6.19.0 \ + --hash=sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14 \ + --hash=sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155 + # via -r requirements-runtime.txt +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via -r tools/release/requirements-build.in +python-dotenv==1.2.4 \ + --hash=sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc \ + --hash=sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0 + # via -r requirements-runtime.txt +pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ + --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ + --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 + # via pyinstaller +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via + # -r requirements-runtime.txt + # ctranslate2 + # huggingface-hub + # transformers +regex==2026.9.29 \ + --hash=sha256:01000ddf0e3ffef97f2413ceb514f6313040106b6d18a03ee00a4fe35c1eb1db \ + --hash=sha256:0166844493626c5015c6088ee15c9ca2fd060ca15b7641d1657da6a58432ae33 \ + --hash=sha256:044265d77d94f5e3cb2fd72c76723807c429cb8c533e9d4672d0334a6f14f588 \ + --hash=sha256:0476e5bcbe6e1ba3d1c4cc7bbb1c3ba78e3b979b5c8a88d0a6a8cdd4992b8c84 \ + --hash=sha256:066d0e3dbfdd739bce2bf8c2a41dd16f73e3d8adc2eb06dd803a36a307f56075 \ + --hash=sha256:0b65c72739f981377c9c22e0c5c3cd7f42da7bd8a3c9209330fac772c7d893ed \ + --hash=sha256:0c992c19cd45058a4b92f68f139c93db168b48fb1f322c9a7cd620806afb6b51 \ + --hash=sha256:0cc63b5e47c12a48d90c7e9d7de6a035dd14f62868aaedbb4e0ff8ba2b8bfe7b \ + --hash=sha256:0dd8af32e9f7b56b7f95cc1fd79b23054c3bdc172392ae560acc24d57b7ffe71 \ + --hash=sha256:0def9fb6abac55492d6d51cddb7225d07d6f279e774e0adc08569a54a5fc8d46 \ + --hash=sha256:0fd2c901cc307a745ad4bc87f20060d7a0825a3371d1e93488af22e7a387f78f \ + --hash=sha256:1043aedf5917caa861bcb25a9c11460049656bdf0017a90a309fa8f255467725 \ + --hash=sha256:121a76a0985db80ceae9e171c337f8c927868e37d01b54e3ce87bc87f9c6a208 \ + --hash=sha256:143533cc4b6fbc5b95aca0a5b8d541088d374831593def000ec89322c220221d \ + --hash=sha256:14e953ff3607c92d7675bf79c4d4509ef6782aa8c08509f179f9b3d6d0679e86 \ + --hash=sha256:18ae8eed4526e35bdb754d61562b90bf5c00a67fdcf3cc1380dd59597486631b \ + --hash=sha256:19959129885356df0e97556856f77eb2888380dac18bed075a7c05c5128c618d \ + --hash=sha256:1ba8c6a416569ce0d37e83e28a254a61dc99a419084dfb6476cea02d997f74fa \ + --hash=sha256:1c2a0026062abcc321a53db4a185ceba0b59a66b5d37b0808917a88b55a5257f \ + --hash=sha256:1d9fe8091b2e89d470df68a9331111ed008ae8aae6bf1e8e1fba4086a495c84e \ + --hash=sha256:2089fe39c406784d90101c726755ffa1497bb74638fd434300d2b88006186de8 \ + --hash=sha256:23ae6fdad9e63e54038f5ef78aba2933faca61e24d432786589e737bc5522ebb \ + --hash=sha256:26ec4ccce55aa533fbd603d08911b01101a8fcfec987845ac3ae2c7087b2bde3 \ + --hash=sha256:2f7f7aa47b229f2b39a2ae2596d2ad5625d77b5eb9856fac2dab3eb506cdd0a0 \ + --hash=sha256:31b003f9a070335e2a8233ee9b14a3ca8e6d792012ae011f741bf0aaf11744c5 \ + --hash=sha256:32ab11df9677ca80bcbb5fe4eb1da9109a5019239a054836efc6fa1c64e683cf \ + --hash=sha256:33026515aebc0e70d1c89978e53e8d695d35d9e472f8d5b34465ba3c74028650 \ + --hash=sha256:34b6925af9853bf461950e6508910f179fd6e9b1a7ec8548e069606b7e51a26b \ + --hash=sha256:352cf115a810b357caa35193ab656ecf5ef41056855e82f292c99e8514f8d954 \ + --hash=sha256:39ab5894d971f9ac68baa6eca5c50387db579cfcacf36ae8df3feceb1815e6d0 \ + --hash=sha256:3a21a9509d0ee88e7a70e1ad228cd2f0e0fd1e187458db132e8a8d18c97daf9d \ + --hash=sha256:3c5c2ef13797466aa64170cbb66ad98a32351dd4127694cea7199f80f213750d \ + --hash=sha256:3e778bfccd63075167709136afbc251c1f683758d5bf49c803c60ac3f894ce6b \ + --hash=sha256:3f1e6cb402a89457582cd696f982559217d13484a193202c394015297968c86d \ + --hash=sha256:42e82e578c904445d4c8a35b8f28052cf567593215fa5db06266fbc6f77aaa2e \ + --hash=sha256:4408b2b27a95ca8cc48b7411945753773353b5c93b307754781086c99d3a576f \ + --hash=sha256:446654b29bfaa30500d80947eda42cef1449dc8a87f4e3cf061cc8485d3a1f0b \ + --hash=sha256:45010bcfe66df41522d56c9b6114e87ecc597a08970ff6a2ced24415c141ae5f \ + --hash=sha256:49ee178ca31c94621294bf9b8b676a92a2e6bba8af0529591753719e57edb621 \ + --hash=sha256:4d7d93613b01b0199961330e49cfc52d479b3d5776c56c691db31130c0a07d91 \ + --hash=sha256:4fb41211d2333eb930a51e0546a65999761cf1f572a4da56ef9b8a62966c06f2 \ + --hash=sha256:4fe97894d1b306c919b4e50def1e6f6c522f4d03a7283811f4d108f1ce5d3ac2 \ + --hash=sha256:554bffadcbcb6d5f4e5fb10a61cc52084b9a63d1dab5f10bcd2c4343972e8e2c \ + --hash=sha256:59b49507f47479e299a9e1bc41b5cb83a7afda0540625f1dbae886615978acbf \ + --hash=sha256:5eeb8edc6110d9194a4d0d54610f64c37a31c605b5dbb7e407fc6ec7fa34a4a1 \ + --hash=sha256:612b709381c0355b70d89cdb51b7f670591ed5cbbc0e3b5337488019dc667b65 \ + --hash=sha256:61956f074ecd123f55adca68ee3eab46e6a07ad3f8e64e6db95dfacb444f55c4 \ + --hash=sha256:6398d5145689503412cc1748895242598d8846b8967b851133b20dc2ed1e21e8 \ + --hash=sha256:65b408d8fcb273e3499e7ef2ce796810da1becd208c7fb4373692a242d79d461 \ + --hash=sha256:686ac5350fceae63830bb98805fcb8039325bf4c06d9f6f048ff65229d5bffa5 \ + --hash=sha256:6a1a824fbed817e0a891103886b68f063b1e83cc51bc97192a90a60195a9291f \ + --hash=sha256:6abb75ab16bc3281714a5b99548a2225db70dba1f995f6d7f7419b76eb5a8fbe \ + --hash=sha256:6f7121a8914ed13fcfe2099f895341bfb789f004d4c5a0bdece8fa667da10849 \ + --hash=sha256:7020ed44df30b3aa492c00ee3b52d0548c1f30c2c6c5bb13ae897680900d3413 \ + --hash=sha256:720537c7ea6f80dc61913184edb0ce2497a306b39ef19f28505b322553d52bdb \ + --hash=sha256:724184b4aafed865e4f13ca313fdcb43024300c028ec67319cfa16847d84685e \ + --hash=sha256:7c03031610e3e6ed1768a2b7a8fc84637c1257b50c5eacaf094c6e17a84fc563 \ + --hash=sha256:80a5ea3b4fd9d6a5b9a44f7976a9acaaab35aa3c1f6b29e5bd857dfabaded223 \ + --hash=sha256:80c7cadd3fd2bfde5df8aa0787e315812cad0c313a753095d02f4c2b6c01677b \ + --hash=sha256:80ea96f5c1a30bf09007d48466521d9c294bebe197c708c3359096e3e3691632 \ + --hash=sha256:864e9b87ac33c3fb9fb4ad48166d4fdb579c351d5c77deb0d34bccb36a775cd9 \ + --hash=sha256:87fb80cbe3557e27e7b28b995c2b2eedf689b8886f941ab93e0e288f0976518a \ + --hash=sha256:8873c4a11c50b9989168881aeb3f08859f469d809941866aa1feefd8be5431f6 \ + --hash=sha256:888d60953908dcf761aa320c3e390ab8556efbdb551ace63921de90f6ae0848d \ + --hash=sha256:8b5fcc4771732191b2b7d1dd68d8f0353f47f8d90b6150f6dce58bf1112442cb \ + --hash=sha256:8f39588af4731c8923c26810eb3b33f76f17633985e40f59c3cd45a33805a895 \ + --hash=sha256:9173db3be74a35cb6731701094b98120f7ee4876a287882a59cdea1fa7da342f \ + --hash=sha256:92f05c9c42bde5785dc48770bc2194d9f7442544156f951e19cd31b096cec562 \ + --hash=sha256:951733b1bbdb71e377cec567b409f1a7881b47cfcad84121aa74cb575fa425ea \ + --hash=sha256:957bb708e8057ab1649ba566456429d691ec9b90d1c9ad1af1ba7ffbbeaf05f2 \ + --hash=sha256:9916fda742cd4eede63b286f58c06718324265d727ce0856eb1aac86d0d150d6 \ + --hash=sha256:9e1d3a4cb7993b708f0ada8d0c84590efd853f169e7147d2202c9da503180242 \ + --hash=sha256:9e4482589065c8ecd761cff522dcd85f2d39e62f551e37e025d1c7d54772def3 \ + --hash=sha256:a5300757f8a68f5b6cc33f57338d72a0e3589c5cc9ad5f8504ea06f028be582a \ + --hash=sha256:a540abfab208e1b7ef2df231c40ef3b6cbb30a0aad6204e9b6a81c10a6794628 \ + --hash=sha256:a5758353650079898dc1b2b0e95aa51fa23a30d020e06f62c430dd08ee56cdd8 \ + --hash=sha256:a64b85a4760337cfefdb27d42da6ed8b58e8cde3f2d57b6ef43e76ef6ea9ef47 \ + --hash=sha256:a655d34b2a6943af32401f3d94f72e9d731f6ad16285815550bf2b4ee69d420a \ + --hash=sha256:a714befaacbd10092ffe4cea0d3c5f008fb9efe9bc322c715bcdfdee414b9a3d \ + --hash=sha256:a760da040b47767b4b873adfb7c3b691e9ba2fc60f113f9d0b88f1a62f323e85 \ + --hash=sha256:addd736a0547d553283adaf4e05d7104e7f2c7b0b092e9b4d28756825f14531f \ + --hash=sha256:ae4613d7d9dda60fcba95f846cc6f808017f1843f392cf9daad14a6534493d71 \ + --hash=sha256:b11b589e00095ec69cf79841a76360f9b079e95b0368a25b5ebb951ab0c157ff \ + --hash=sha256:b3e445b66c80b4eb4234e855ce94d9adc183eedbd632816228d89930b91b2c5b \ + --hash=sha256:b7b893976e7fe42053da64f2aa27239c24252fd2ec6df471e1be197c0addc3b1 \ + --hash=sha256:b84f186a7f0536fe4ff9a9fa12d06d007b9b71d4b5352ddcc41f59ad6522a312 \ + --hash=sha256:b89efc38431793d28b7cd91227e2f952ad7c48df19132b17f43a5fec3c14143b \ + --hash=sha256:b97a38fb4c732b6832db6bf108963adbcd82ef1268ba2025dce390f45af75efa \ + --hash=sha256:b9d74e4eee9ddb64c2e92d5d61472c59c21684c059eb7b68767be9628e977859 \ + --hash=sha256:bb90e7177944b6684738c1fc36aabd2dd00d1de3be7dbe09f91e196f1bc0dc81 \ + --hash=sha256:bec37990e3d6121f29ecfb594bd8f1bf009e9f7926daba2e50e3b27d3892a783 \ + --hash=sha256:bf3c49863c23a1ad6da9c30351aed6cff8d5ddbeb63c5c8420ae54e98c7d0138 \ + --hash=sha256:bf48516e35cf848390ea68850aba53e7c333720d2945b4d2c25b69fc5171723f \ + --hash=sha256:bfc71e6d970419c1309b3640305298643e2a734cad3f7cfb6d2ddee4175ab53d \ + --hash=sha256:c0094897d7d01f184b2d7fe8c56c66d64efe01b31f4b7d34205b391387df1111 \ + --hash=sha256:c03c6eb6ece86dfdcbb34799efaa339b093132e1aceed491ba5e08fe06cdf699 \ + --hash=sha256:c1a9a6651197fbed6f0212591418b9def774fc3f8324f78d1bf0e6a63e5f8aa1 \ + --hash=sha256:c3589f40749acce747510bf5d589d54e376cb0930ea58b35effac97e5312b0c1 \ + --hash=sha256:c4e38dd8f39c43a91d2410ad2b85610701b0979342c3df1d69eaf8e838c757d8 \ + --hash=sha256:c6c8fabf1dafc1f1ddcbb67896d3f93efb092e8c4b6322d7389b944e76a484e5 \ + --hash=sha256:c90fcf7804ea0a54b896ce0f2b9565350220b8d4890fd0db461a476a4c687963 \ + --hash=sha256:c9b602fae1e00b7c035d661ce85575365719192a7b46784bd71cf64c68053aa0 \ + --hash=sha256:ccb64d887a9db1cd76dbc0f92051a1a478a2a67e7f56c62d915cb881d7734704 \ + --hash=sha256:d06fcdecc10fc7954d7c8f27a03c96055fe525274dc84a7b0dbdc3d6b9e03dab \ + --hash=sha256:d0c3082bf79bcd6a614d55916590ad4b8f93200e10b97f463ea5d9d07c9b5f23 \ + --hash=sha256:d49c18f1ea294cf4adde2e5ac256e98c82ea9d708462ce4bf799dffa7cfe8a2c \ + --hash=sha256:d60030baaa7bfbb02d650c126cdcddcb6e33dbff14d819434c8fa2fdcaeeeba5 \ + --hash=sha256:d7cab119d0df0b9413f106b4d7fc34f2872d3574ed3806fb48959c830b1537da \ + --hash=sha256:d9b77b25b4f395f92de6099ab08e8ae2bc7e51dfe157f22900902243a5cc90c7 \ + --hash=sha256:dabee8f4935e731fb46b2a3091bdda0d3d94b3bbfb907d2b4f12eefce4009619 \ + --hash=sha256:db5e82ba15c142425b8406690032df89e39cca4a2e8afbbb9a3d84edc2373ac3 \ + --hash=sha256:dc79d36d0618752265f0d575915bdc5c5130ecb9c9f6b3bcefeae32e4bdfafcf \ + --hash=sha256:ddfa987262763c3c22a8367d2a49c244b018a74c3a8e3ab1a864119ad45c5633 \ + --hash=sha256:e1172147d28d8fbcf8cb8d26c41506169f5ad8fe9ec969cb116835a19d4d8eca \ + --hash=sha256:e11edba5bc344a32b029a7af9d4b3173982dd79eeafa0b9dbd787364414b0509 \ + --hash=sha256:e2c89e9b762c57f59d5e99ee8b20202adb892e35f8d3485741340999ca55058e \ + --hash=sha256:e31f72490b7c12f7790e1e25c3afffd20503ee1bfb43461d7838b871ff244b19 \ + --hash=sha256:e8c65ef3862a8ad6e86492b6ed9327805dd66904c012bd3649dc67d822ed6c34 \ + --hash=sha256:ebb8912f565b8cdbbf27debfe00df04202c20e2f651b9e32767930c5eace3621 \ + --hash=sha256:ed511a0708e2297e1d6431e7fb217e3402791e491e02da800658ace4973df1bb \ + --hash=sha256:edf06545875f3efa31560d94121e95c7fd70d98b1dfedc0157097d79b13b52ea \ + --hash=sha256:f0fe9834e5aeccaf19a0d8feb296d66a24be1a7c9922002f842a682cd5abb787 \ + --hash=sha256:f1a0d5117230dd46b399a30a38afa44f79c99f3168988fdc4f425c3f928b39df \ + --hash=sha256:f37964e4a5e993d2fd45147741e9dff7f34a2d8c00ab94c4ea0514a4677f959e \ + --hash=sha256:f57dc6b8fef170f105d2cf5cdce254f47b137d7755086cf7050f47e16582abba \ + --hash=sha256:f93bc1c3486ef3747e07c9d7c1d0a147b8fbaab975f80e348aed6f71309dfaca \ + --hash=sha256:fb00027a09a8f9f08028b40dce4c933cf73e4833240ed356583fdc9cfa721566 \ + --hash=sha256:fb99cc9d45f48895d9d67f6a0b8a57f08d39c174d9f25ad97a313e0470267b1c \ + --hash=sha256:fdd88ed5e20b1bcdd234421e454962c971aa44b653bdb7f1ea9ef683e90fb649 \ + --hash=sha256:fe3fa1dd453ed5c7f5ea23a26218329790ed7197a99b90e94330e313959a7f52 + # via transformers +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via -r requirements-runtime.txt +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via typer +ruamel-yaml==0.19.1 \ + --hash=sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93 \ + --hash=sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993 + # via -r requirements-runtime.txt +safetensors==0.8.0 \ + --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \ + --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \ + --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \ + --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \ + --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \ + --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \ + --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \ + --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \ + --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \ + --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \ + --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \ + --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \ + --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \ + --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \ + --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ + --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ + --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 + # via transformers +scikit-learn==1.9.1 \ + --hash=sha256:09f4d73049cd63575157f6b1060e06a8c83a4bd3488dbfaeedf35ccba7aad712 \ + --hash=sha256:0c0f8b5d09b44101cea2767f300680bada1ea27f976fe4b48b83950a4f55a49a \ + --hash=sha256:2070f271e5375dc42c6bb93b461ab1c0aa5841d4009267e0cfd95a39dca94a43 \ + --hash=sha256:220fa18152852a5ce29c49e1eaba9d44ec44631cd2e5cf65f5a40eafa5ab3412 \ + --hash=sha256:2c2b312fd8c02951a364fa120ea08c1cec10d863466bf1701b013152d7537835 \ + --hash=sha256:326c188f92084bf58664229f4578eeab6176313b37cd5dfc85abd92b94130c58 \ + --hash=sha256:38cd925e893e5539be704d5edc64dbe081aacdab6b89d8c2977c1f6a7a453ce5 \ + --hash=sha256:4298fcc01b3d8fa9768d36894e99cce0747b3b2dd73bfd80779e393769d0afab \ + --hash=sha256:48fefd8eb42bd4eec3e2d348149368ccd6d71987e20c30706a56a24eb86a6e73 \ + --hash=sha256:4b59abb30618121cc46b45972d6bf53a7128b4df4cd346c6ca6f4d5f9031e49c \ + --hash=sha256:52a0703bbc07ad27f560fa63fa68e4c54dd735bfbbf65b4dd3c225dc7547b6df \ + --hash=sha256:52cfdb1fed3a34362dbc0bd96f2e761a66fd5724d6901629f5a558f1f3bd9849 \ + --hash=sha256:55e79d6e9b0923f1a978179822bd43d7f5543f45e970a00fe861f43486380aba \ + --hash=sha256:5990f9c69e431bfaddcde1a6d7c5355243e026bc9b9e560c13893b90dab53fb4 \ + --hash=sha256:613f0a783ca05aa844a4e1ac42d48425058f2c52be73f40f8cd98b7cd111acd6 \ + --hash=sha256:61cd968ab831a76d0ecbaf0347ab2270268716da28f94fd022497e3d6f205f13 \ + --hash=sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977 \ + --hash=sha256:66f852f7325b5070bc28329005aca76055a2def78faac039548ae889aeaa45a6 \ + --hash=sha256:6754b7cabfc3df0b1f7b38f7a344f559bbae9d82f0ac5e3d48cccbd19fdcefdf \ + --hash=sha256:748bcb0a4cc04aec470652c9e5ec68450948e867387e7dfade647107ade68d25 \ + --hash=sha256:7b5cad1624de8b75e5b9ccb7b0ce1ff1d01306340a3efc56d5529c5ba92392eb \ + --hash=sha256:800dd22dd87fe97dcea484c24e85dd93cf1734d86bd74e668ad18f7967f4d1b5 \ + --hash=sha256:8218cb8938d3031e0d23842838425490c229ecf3e99f666776e09d12ed902352 \ + --hash=sha256:8893bc6331f60f18d4ac75e12ed356e2dcf6a564bf767918b5b7ca54c8c8be49 \ + --hash=sha256:8c14ce41d561f7749f990b41d6703fe02c4669fbc485e598e069e0a1967b488e \ + --hash=sha256:8ca869d0080a5723cde2d5a8b54a2da1ff7e68735a9e9adb3da1243183a0fa01 \ + --hash=sha256:8dec64f31a6e0ec826aca6c1b39a51e16d946e400d4c0904316f3ca72ccfb825 \ + --hash=sha256:90de6573f733a9fb79476ff1371af52a397d41c8b35f9146e20923db010d67b6 \ + --hash=sha256:993d332ff80e62efae9e39603b7e872297c418d780f01a01855269a3489c950f \ + --hash=sha256:ae6571a4828c6f5019bcd2b4125e5b18c0af3dbc9c99726c891f45f41335ec8e \ + --hash=sha256:b01e5b01735d38474127ca3f49319b592506225a87793b27559816b5c75cea39 \ + --hash=sha256:b3da53831534214322d9cb240fa6f390b36cf69eba727a6d4bd3238677630d70 \ + --hash=sha256:b48b2b5b41d9c5fbafef5f37b042f61110df3318ad2a45baf57287ea5b9ba5a2 \ + --hash=sha256:b5492cf2df5226691c32611de8734bcf42148c6547ae53c7f4e6b847793addc0 \ + --hash=sha256:ca9051447455dae341d4d591eece7deb2d8e3d1020298fc87a81fc51e4da8f53 \ + --hash=sha256:caae15634feceafa2612566b109a3082d3293167fac388eedaf77bff66b51983 \ + --hash=sha256:d137ce8a6142029fb5c35bd82f470c40cd9e760e5e2f7694b362c497c4ab3fa2 \ + --hash=sha256:d5945a2908be62350e2978344e62b56c1552c2ca4f844ebf6277c94944d647dd \ + --hash=sha256:d5d117952769b563067656784e03c75a2d8235a7a05cf7fffa78a311e75aac08 \ + --hash=sha256:e1b468241f4a7a9a7a0d6479ad3cc47681cc151a4046c530f2777c3d68f08942 \ + --hash=sha256:e4c20a6c017d820faa7ac8c783e3d0c6a9a2e297bf9f55332ca17cdf7fd4d04d \ + --hash=sha256:e5d7b18a5b9dca241a74695f3275fa4c895a9dadc72b3d8df5fa9d1083c9b83e \ + --hash=sha256:ffbcbbbb44202fbe9bc64bced25a145759adb9ef010b3d37a8064958ac13df2a + # via sentence-transformers +scipy==1.17.1 \ + --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ + --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ + --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ + --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ + --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ + --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ + --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ + --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ + --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ + --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ + --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ + --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ + --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ + --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ + --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ + --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ + --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ + --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ + --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ + --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ + --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ + --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ + --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ + --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ + --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ + --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ + --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ + --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ + --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ + --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ + --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ + --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ + --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ + --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ + --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ + --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ + --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ + --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ + --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ + --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ + --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ + --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ + --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ + --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ + --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ + --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ + --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ + --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ + --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ + --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ + --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ + --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ + --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ + --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ + --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ + --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ + --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ + --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ + --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ + --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ + --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 + # via + # -r requirements-runtime.txt + # scikit-learn + # sentence-transformers +sentence-transformers==6.1.0 \ + --hash=sha256:299025df51550dc1a38f05be27a9b0bf881c4e5e70542b3b7757d05e00aa3868 \ + --hash=sha256:eb8122f4d180f552eda26dc3d77e84e8c11dc2b1d456a406b9f24abb70ceeadd + # via -r requirements-runtime.txt +setuptools==84.0.0 \ + --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ + --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 + # via + # pyinstaller + # torch +shellingham==1.5.4 \ + --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ + --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de + # via typer +silero-vad==6.2.3 \ + --hash=sha256:7b7f5436cfcb02fae583a05b512ea96467fd449fe54cb49a5e4f06c51a1e43b8 \ + --hash=sha256:d7a23b09202be4953915cb9173089bb307eed0418a9bd4794d9ec5af07fa1996 + # via -r requirements-runtime.txt +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via openai +sounddevice==0.5.6 \ + --hash=sha256:7f4162f514f007b0bf25a3ccfed3f1705bc2ec311888a90232729eec4f57a4f4 \ + --hash=sha256:8ec9fbfde2e32f020b167e348f3ab3bac6625a5f15af524d790108ac7147a410 \ + --hash=sha256:b36b807eb02abd257198bf84b2af05e4fea199a9d2f0019014169c7136d45e9c \ + --hash=sha256:c8ae19173e5f27f8c12d4b5eee2dbfe542cee125d591e663e0fb4dfb75246d45 \ + --hash=sha256:de099612311ad81e55d31ccbd83f43ea6bf4d87b48f9b6ea55a1fbcde0eee4e0 \ + --hash=sha256:e3aef00ad8b1d1740eb66d9a7671eab88a4d2b8fa4ab33498d742e63b65c309c + # via -r requirements-runtime.txt +soundfile==0.14.0 \ + --hash=sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849 \ + --hash=sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4 \ + --hash=sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d \ + --hash=sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e \ + --hash=sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8 \ + --hash=sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11 \ + --hash=sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c \ + --hash=sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98 \ + --hash=sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377 + # via -r requirements-runtime.txt +starlette==1.7.0 \ + --hash=sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e \ + --hash=sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d + # via fastapi +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + # via torch +threadpoolctl==3.7.0 \ + --hash=sha256:61348cfb77d53b9242e0017029244b559b810c142ced65b4e21eeca1843959a7 \ + --hash=sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be + # via scikit-learn +tokenizers==0.23.2 \ + --hash=sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78 \ + --hash=sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde \ + --hash=sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7 \ + --hash=sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305 \ + --hash=sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb \ + --hash=sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718 \ + --hash=sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5 \ + --hash=sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac \ + --hash=sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90 \ + --hash=sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703 \ + --hash=sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf \ + --hash=sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2 \ + --hash=sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef \ + --hash=sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a \ + --hash=sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa \ + --hash=sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40 \ + --hash=sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835 + # via + # faster-whisper + # sentence-transformers + # transformers +torch==2.14.1 ; sys_platform == 'darwin' \ + --hash=sha256:17b32fd68abe9429d936c643579e24465e7174a06c168bbfcc01395a881bc8af \ + --hash=sha256:3e4d7679866c4cd68258ccda7c51bcda150797eda095813f26fe63c07934cc21 \ + --hash=sha256:6fe25964e53113a8a1f5640be1103a982a426c07f8907fba48fcca1be1112202 \ + --hash=sha256:729992e6b581de722989012547b7767ef8ac019bd060e34805f06a52ff9a120e \ + --hash=sha256:7b0670b545f1b732a3219966e6704b27398a32a764bc7cce065e40b142dcf8a1 \ + --hash=sha256:82762c7527079f1e17a771684362ed51400e3bc72942df30217e40f27c6816d7 \ + --hash=sha256:9cf3082d25560efb1eef921871595227c0cf305abb7761ca45b6b988ef6cdd45 \ + --hash=sha256:e3ac0a862f1e81a23bd2b9ff979c189a2154e731c06d4c530bfbf2642ff01ddf + # via + # -r requirements-runtime.txt + # sentence-transformers + # silero-vad +torch==2.14.1+cpu ; sys_platform != 'darwin' \ + --hash=sha256:100df7e44af5c973606cfef571a14b36b0acfd165d6bbc7aabe3edac95c9ccbd \ + --hash=sha256:192745b92c9ac392298e73de950cd02d8a7886117923073d890625622d343ede \ + --hash=sha256:24b279ee181df14dc353f3e3f0e3f088d88a38175989dadb5645c0194830d4de \ + --hash=sha256:2547c9f32f8fd9c8fced83119269d85519b123d4e8c87eba71660fe3a6996336 \ + --hash=sha256:2948f065d848178a0402791baf87493b7ebf067dae48a8e04c498d6668e94895 \ + --hash=sha256:2af649994b36dffc627175c3f306d9e513a309b58d4619ac7107577613336454 \ + --hash=sha256:2f8162c6b265eaf2b0bdd6bef20e96cece430f308e1477a32d0aef51e472124c \ + --hash=sha256:331fa474e26e428e2e6af1143b2fdab19cee492d7d21897b8899ce1c14755662 \ + --hash=sha256:35103e180214793207f95d7e12e100ecba3ace6f5fb37e1fcefbb07e096ed180 \ + --hash=sha256:36a03a3f87ec875ca39c8e29eccbaf0fc704605f78a3f7f179f157048c3c395e \ + --hash=sha256:4b323c71a5ac184485c87d694842157b1eb217c425ad69761dafd379d1590521 \ + --hash=sha256:4ce2a6ae7d302bba56ef7c5c92d9f752728896cfe2626a7f3e5293f879e36650 \ + --hash=sha256:4eec5a4fadd7a18f9c75b97fdb374fbb579ce4b1604bf66eeb14dc2201d54def \ + --hash=sha256:5a6363570c753812540a05eb82380e329469cbe668643e88111414c12627711f \ + --hash=sha256:5c0d1ef51127c9c65ec6394226b82ef1a5f3cd0ab8ffb10f48cc5c08aa72a5cf \ + --hash=sha256:5e38154c8896d426a5df58bf2276b603e82184dfae4f54d77dbb212f36e34f37 \ + --hash=sha256:6b06a7f6275c5fdecaf1dbf5546f0820b024a97785575bb99e3d9930d83cd86b \ + --hash=sha256:783a3dec10d272e40ed3f1a05ec228400300d3229b8f7521a9327645fae88646 \ + --hash=sha256:7d2c35e915793f3ae82e8ee4041a960140bcd0bd35f3ec6eb06e1f1f37a89dba \ + --hash=sha256:88634fad89f7b953acd011bad0d5c131d269aa71a7de6e0ef2344e3a2c95fabc \ + --hash=sha256:930d724490955b822c0b005499128279e54876a318b51fdb9b9da35e0230cc0f \ + --hash=sha256:94edee897fbd15a73f91aa38e22e2d2b7b60ac3acf6e8afc6b8dad89229c810c \ + --hash=sha256:a46884e8b38cbdf5af8d4fdd39223a5fa97491419c3417c93f9b3e28749fa356 \ + --hash=sha256:a7a580e3d92467516c9247a22bc805ae4f79c7b1b9a2052f92849f34e89720f5 \ + --hash=sha256:a93db7947d53caab62c2436205a60ddf9c3150c2e09eb9b24610c1c3f4cbaf99 \ + --hash=sha256:b869fe9358c90dd82d02db89aef89729df93a89a48b7c3e21a7719abf55586b3 \ + --hash=sha256:bda7043ec45539b1b61538af468e19854b72ac6be8d6fde2f51dfd0c91721568 \ + --hash=sha256:c52adc3bd526ff7fc5bdcc58fc4a76e6214d09951b646ffe5256ebd7ba60cef1 \ + --hash=sha256:cccd0a609fe3f83ab2ff651b627d5bd68c7fb33ee77a994b31c18b7c9a6f9a52 \ + --hash=sha256:d14b0c8a66345b7ba43adfa86fc20d89087a0a92e440004755d49d2468ddc6e0 \ + --hash=sha256:e799161f94d97485857ec82424e40a83d7648ab02c03e3d6e71c7838a4d7b89e \ + --hash=sha256:f56cd23a3bf003bfbed3da0645ba09b3ccc66c0a2f2ac726a9b98f34e0e4926f \ + --hash=sha256:f67ee1fe9c9f7c6ad3785d150bc3cecda442a5880a6d40816e13fee475d97dd8 + # via + # -r requirements-runtime.txt + # sentence-transformers + # silero-vad +tqdm==4.70.1 \ + --hash=sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73 \ + --hash=sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4 + # via + # faster-whisper + # huggingface-hub + # sentence-transformers + # transformers +transformers==5.18.0 \ + --hash=sha256:d79e5a515a572ee3deb33eb0d578be47d91e70c75d54cf21bf6eede6edef14a9 \ + --hash=sha256:d89c206e42e841af7cd314b3d2b21c03e977d819cdb4aef65ada048769a644df + # via + # -r requirements-runtime.txt + # sentence-transformers +truststore==0.10.4 ; sys_platform != 'emscripten' \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typer==0.27.2 \ + --hash=sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945 \ + --hash=sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d + # via transformers +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # aiohttp + # aiosignal + # anyio + # fastapi + # httpx2 + # huggingface-hub + # openai + # opentelemetry-api + # pydantic + # pydantic-core + # sentence-transformers + # soundfile + # starlette + # torch + # typing-inspection +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 + # via + # fastapi + # pydantic +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 + # via requests +uvicorn==0.54.0 \ + --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf \ + --hash=sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620 + # via -r requirements-runtime.txt +websockets==17.2 \ + --hash=sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3 \ + --hash=sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b \ + --hash=sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348 \ + --hash=sha256:063508ce9e0db745f30ab52fc652f4e59efc79c2b74934b3837d5cdb974da620 \ + --hash=sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1 \ + --hash=sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d \ + --hash=sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268 \ + --hash=sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735 \ + --hash=sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0 \ + --hash=sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0 \ + --hash=sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a \ + --hash=sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668 \ + --hash=sha256:15a7101b660a9f15fac34108c92cefc9848f6753a50acef8869e3cd94148fdb7 \ + --hash=sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512 \ + --hash=sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4 \ + --hash=sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b \ + --hash=sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2 \ + --hash=sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121 \ + --hash=sha256:214da56dba368f61b3d745c77630b2d03c61c02da7b42fe80ef6efba079d3077 \ + --hash=sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784 \ + --hash=sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83 \ + --hash=sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa \ + --hash=sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3 \ + --hash=sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a \ + --hash=sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba \ + --hash=sha256:2de1ccf298f5c9e0f27113836d742edb95f015eee3148f004ac386f7ba9a05b1 \ + --hash=sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428 \ + --hash=sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe \ + --hash=sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8 \ + --hash=sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7 \ + --hash=sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725 \ + --hash=sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158 \ + --hash=sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4 \ + --hash=sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792 \ + --hash=sha256:376a693697ddb695ea282ead76060f4847f90e564b12b4389f2c7589e6fadb9e \ + --hash=sha256:3892d76754b5f36fb40619f3ef09c68e5c3091f1ab8840964518ae5a41f30952 \ + --hash=sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6 \ + --hash=sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7 \ + --hash=sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9 \ + --hash=sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9 \ + --hash=sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a \ + --hash=sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8 \ + --hash=sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc \ + --hash=sha256:4733fc2d99fe888261417b7e29995403a72d9ffa78629902882325ea141177f2 \ + --hash=sha256:48997ed4431d8006988788ef4b62e1fd3f053c7463b4fa793aa6c4f9e96a3bb7 \ + --hash=sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd \ + --hash=sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163 \ + --hash=sha256:4e312e07557a5ad348f4e83d3419773527f6e790c7f97928b1911d767b6ea1c7 \ + --hash=sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f \ + --hash=sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808 \ + --hash=sha256:5436ffea003adb50e283ca0684a3fcaa1396104f841736c3322ee6582bd09e98 \ + --hash=sha256:55c5b9eab079540bfb639b40b07b7b467e5c5a7ecf97a65cc8665781381c9856 \ + --hash=sha256:55f9a808a0e072473337c240c939849818276e288e2374b832255b5b791b0851 \ + --hash=sha256:569ed5db651e420b13279f9333443bb5b84a436cc66b599cbc535697ae4434a0 \ + --hash=sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547 \ + --hash=sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94 \ + --hash=sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559 \ + --hash=sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7 \ + --hash=sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18 \ + --hash=sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c \ + --hash=sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6 \ + --hash=sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2 \ + --hash=sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0 \ + --hash=sha256:660aa158127035e741d4b1835dbe79ae18a1fbb21ecd236655f31d60110e68d5 \ + --hash=sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b \ + --hash=sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37 \ + --hash=sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae \ + --hash=sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a \ + --hash=sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8 \ + --hash=sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318 \ + --hash=sha256:761cde41439f0be761aa460e1451a31e2e14baf4a46db6fe4913e5a06a90df66 \ + --hash=sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6 \ + --hash=sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd \ + --hash=sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76 \ + --hash=sha256:80cbc645af23ac5c12096545c161626960114a1bc10f864760558d3b3e82ba18 \ + --hash=sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428 \ + --hash=sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6 \ + --hash=sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a \ + --hash=sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d \ + --hash=sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d \ + --hash=sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc \ + --hash=sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997 \ + --hash=sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774 \ + --hash=sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614 \ + --hash=sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b \ + --hash=sha256:902ce8cafca2dc14cef9558a6fc3b45dbf7f121d1404bf2ad18a1c894555e48c \ + --hash=sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507 \ + --hash=sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090 \ + --hash=sha256:946ac2164d646e733004946ae39536b5af473853183d81da5962e29d36e3ad35 \ + --hash=sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18 \ + --hash=sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1 \ + --hash=sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3 \ + --hash=sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f \ + --hash=sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d \ + --hash=sha256:9c393a202df08e96ed619310f0cd78be700e532a57d9a6ceee5f80b4e35bef14 \ + --hash=sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556 \ + --hash=sha256:9df9d048def11365d170b375b6ffc8b23a7f188c3560acd4418ba088ca2e2705 \ + --hash=sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e \ + --hash=sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40 \ + --hash=sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e \ + --hash=sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f \ + --hash=sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40 \ + --hash=sha256:ada04d0262ab06527054a2a497f384d102698ff39b3865dc566a7d24b6f4058c \ + --hash=sha256:af4c565b923bb5975401b8e4cedc2e17b2fdbf33b905737ee12384e6a6fd9507 \ + --hash=sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12 \ + --hash=sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1 \ + --hash=sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326 \ + --hash=sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c \ + --hash=sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d \ + --hash=sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd \ + --hash=sha256:c2ec7e51157a3fa0e9cfdb1a8969bab38d1c22ad1ace7c6cea006383b43a1ad4 \ + --hash=sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a \ + --hash=sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af \ + --hash=sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6 \ + --hash=sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521 \ + --hash=sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f \ + --hash=sha256:c81d6cdbacccda7e0eef3b076a457fd14c3835cdbc5993d2881580c2fb1f5f26 \ + --hash=sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a \ + --hash=sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8 \ + --hash=sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e \ + --hash=sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9 \ + --hash=sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d \ + --hash=sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c \ + --hash=sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d \ + --hash=sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef \ + --hash=sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2 \ + --hash=sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563 \ + --hash=sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a \ + --hash=sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa \ + --hash=sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc \ + --hash=sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e \ + --hash=sha256:e53d950e16d4bb672a5ff41fe3131e65a4e5d688d694e1c7074c8c9990bb3ceb \ + --hash=sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59 \ + --hash=sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a \ + --hash=sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e \ + --hash=sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e \ + --hash=sha256:ecd63d0c7ed0d3d719c91b5a3861f0f0b3cec9bf223033ddf69d17aaac74bb6d \ + --hash=sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5 \ + --hash=sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb \ + --hash=sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8 \ + --hash=sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f \ + --hash=sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1 \ + --hash=sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31 \ + --hash=sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a \ + --hash=sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd \ + --hash=sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec \ + --hash=sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48 \ + --hash=sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f + # via -r requirements-runtime.txt +yarl==1.25.1 \ + --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \ + --hash=sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7 \ + --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \ + --hash=sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa \ + --hash=sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62 \ + --hash=sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec \ + --hash=sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26 \ + --hash=sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651 \ + --hash=sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453 \ + --hash=sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6 \ + --hash=sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc \ + --hash=sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287 \ + --hash=sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426 \ + --hash=sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868 \ + --hash=sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde \ + --hash=sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9 \ + --hash=sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7 \ + --hash=sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0 \ + --hash=sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf \ + --hash=sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95 \ + --hash=sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5 \ + --hash=sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6 \ + --hash=sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94 \ + --hash=sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a \ + --hash=sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57 \ + --hash=sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c \ + --hash=sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee \ + --hash=sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b \ + --hash=sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b \ + --hash=sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c \ + --hash=sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26 \ + --hash=sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c \ + --hash=sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184 \ + --hash=sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8 \ + --hash=sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58 \ + --hash=sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b \ + --hash=sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d \ + --hash=sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2 \ + --hash=sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c \ + --hash=sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f \ + --hash=sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3 \ + --hash=sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa \ + --hash=sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3 \ + --hash=sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb \ + --hash=sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707 \ + --hash=sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c \ + --hash=sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345 \ + --hash=sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40 \ + --hash=sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492 \ + --hash=sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371 \ + --hash=sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec \ + --hash=sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c \ + --hash=sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c \ + --hash=sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25 \ + --hash=sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac \ + --hash=sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6 \ + --hash=sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3 \ + --hash=sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459 \ + --hash=sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03 \ + --hash=sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40 \ + --hash=sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8 \ + --hash=sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209 \ + --hash=sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54 \ + --hash=sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d \ + --hash=sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126 \ + --hash=sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d \ + --hash=sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9 \ + --hash=sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0 \ + --hash=sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225 \ + --hash=sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414 \ + --hash=sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059 \ + --hash=sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d \ + --hash=sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233 \ + --hash=sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b \ + --hash=sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4 \ + --hash=sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273 \ + --hash=sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc \ + --hash=sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865 \ + --hash=sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc \ + --hash=sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4 \ + --hash=sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1 \ + --hash=sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3 \ + --hash=sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee \ + --hash=sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a \ + --hash=sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df \ + --hash=sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f \ + --hash=sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d \ + --hash=sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449 \ + --hash=sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e \ + --hash=sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d \ + --hash=sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d \ + --hash=sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35 \ + --hash=sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420 \ + --hash=sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b \ + --hash=sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68 \ + --hash=sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe \ + --hash=sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd \ + --hash=sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b \ + --hash=sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8 \ + --hash=sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9 \ + --hash=sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908 \ + --hash=sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85 \ + --hash=sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f \ + --hash=sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64 \ + --hash=sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab \ + --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d \ + --hash=sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca \ + --hash=sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1 \ + --hash=sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7 \ + --hash=sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3 \ + --hash=sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa \ + --hash=sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57 \ + --hash=sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2 \ + --hash=sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743 \ + --hash=sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e \ + --hash=sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487 \ + --hash=sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17 \ + --hash=sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0 \ + --hash=sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f \ + --hash=sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6 \ + --hash=sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3 \ + --hash=sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad \ + --hash=sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d \ + --hash=sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1 \ + --hash=sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89 \ + --hash=sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc \ + --hash=sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f \ + --hash=sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3 \ + --hash=sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a \ + --hash=sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4 \ + --hash=sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98 \ + --hash=sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258 \ + --hash=sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b \ + --hash=sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f \ + --hash=sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b \ + --hash=sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326 \ + --hash=sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc \ + --hash=sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75 + # via aiohttp From ea8d044ac1baf53ecb2b6df4d3966dec7aacdefa Mon Sep 17 00:00:00 2001 From: Mayank Shetty Date: Tue, 6 Oct 2026 17:38:44 +0530 Subject: [PATCH 11/37] Wave 3, segment 3: one native-backend manifest, per-OS release builds, macOS DMG - V023/V207: electron/native_backends.json names every bundled backend (cuda, vulkan, metal), the platforms that ship it, per-OS library names, shared-library patterns, rpaths and CMake switches. config.py (bundled:), release.cjs (hardware detection runs only the current OS's probes, none on macOS), first_setup.jsx and build.py read it, and naming is tested with explicit per-platform tables on every host. - V034: build.py builds the manifest's backends for the host OS: Metal on macOS with @loader_path rpaths and .dylib collection (versioned links kept), CUDA and Vulkan on Windows and Linux. It also puts Code/ on PyInstaller's path: --collect-submodules process silently froze none of Riko's dynamically imported modules, so the frozen tool worker never worked on any OS. - V057: an arm64 macOS 14+ DMG with hardened runtime and the three entitlements it needs (allow-jit, audio-input, disable-library-validation, each proven necessary), a microphone usage string, an icon rendered from assets/logo.svg, and ad-hoc signing unless Developer ID secrets are set (retried like electron-builder's own signing). The packaged app asks for the microphone before starting the backend and says once when access is off. release.yml gains a macos-15 leg and repeats the release check inside the signed app (build.py --check-app). - Electron's runAsNode, NODE_OPTIONS and --inspect fuses are off, so other processes cannot relaunch Riko as Node to use its microphone permission. - Checksums cover only shipped folders; macOS notes that signing changes them. Built end to end on an M5 Pro: Metal bundle, frozen backend passing --release-check on its own and inside the signed app, and a 547 MB DMG that mounts and verifies. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 36 +++- CLAUDE.md | 10 +- Code/process/app_core/configuration/config.py | 8 +- .../app_core/configuration/native_backends.py | 112 ++++++++++++ Code/run_server.py | 4 + electron/build/entitlements.mac.plist | 17 ++ electron/build/mac_sign.cjs | 22 +++ electron/electron-builder.yml | 21 +++ electron/main.cjs | 16 +- electron/native_backends.cjs | 15 ++ electron/native_backends.json | 50 +++++ electron/release.cjs | 39 ++-- electron/src/first_setup.jsx | 4 +- electron/src/mac_sign.test.mjs | 17 ++ electron/src/native_backends.test.mjs | 38 ++++ electron/src/release.test.mjs | 87 ++++++++- tests/test_release_build.py | 171 +++++++++++++++++- tests/test_release_config.py | 98 +++++++++- tests/test_settings_store.py | 14 +- tools/release/README.md | 24 ++- tools/release/build.py | 164 +++++++++++++---- 21 files changed, 880 insertions(+), 87 deletions(-) create mode 100644 Code/process/app_core/configuration/native_backends.py create mode 100644 electron/build/entitlements.mac.plist create mode 100644 electron/build/mac_sign.cjs create mode 100644 electron/native_backends.cjs create mode 100644 electron/native_backends.json create mode 100644 electron/src/mac_sign.test.mjs create mode 100644 electron/src/native_backends.test.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 229b717..d15fb29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,8 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2022, ubuntu-22.04] + # macos-15 is Apple silicon (arm64): it builds the Metal bundle and a DMG (electron/native_backends.json). + os: [windows-2022, ubuntu-22.04, macos-15] runs-on: ${{ matrix.os }} defaults: run: @@ -39,6 +40,7 @@ jobs: echo "LD_LIBRARY_PATH=$RUNNER_TEMP/vulkan-sdk/x86_64/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" LD_LIBRARY_PATH="$RUNNER_TEMP/vulkan-sdk/x86_64/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$RUNNER_TEMP/vulkan-sdk/x86_64/bin/glslc" --version - name: CUDA build toolkit + if: runner.os != 'macOS' uses: Jimver/cuda-toolkit@v0.2.24 with: cuda: '12.4.1' @@ -52,7 +54,8 @@ jobs: "VULKAN_SDK=C:\VulkanSDK\1.3.296.0" >> $env:GITHUB_ENV "C:\VulkanSDK\1.3.296.0\Bin" >> $env:GITHUB_PATH - name: Python runtime dependencies - # Exactly the versions in the hash-locked files from tools/release/lock.py; torch is the CPU build. + # Exactly the versions in the hash-locked files from tools/release/lock.py; torch is the CPU build. On macOS the lock + # names the plain PyPI torch wheel, which --require-hashes selects even with the extra index. run: | python -m pip install --require-hashes -r tools/release/requirements-lock.txt --extra-index-url https://download.pytorch.org/whl/cpu python -m pip install --require-hashes --no-deps -r tools/release/requirements-lock-no-deps.txt @@ -63,7 +66,8 @@ jobs: env: RIKO_RELEASE_BUILD: '1' run: python -m pytest tests/test_release_build.py tests/test_release_config.py tests/test_llama_native.py tests/test_responses_wire.py tests/test_settings_store.py tests/test_dependency_manifests.py -q - - name: Build both native backends and freeze Python + - name: Build the native backends and freeze Python + # CUDA and Vulkan on Windows and Linux, Metal on macOS: the backends electron/native_backends.json ships for the OS. run: python tools/release/build.py - name: Frontend dependencies and tests working-directory: electron @@ -74,13 +78,35 @@ jobs: working-directory: electron env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' + # Optional macOS signing secrets, given only to the macOS leg. Without them the app is signed ad hoc + # (electron/build/mac_sign.cjs) and Gatekeeper asks users to approve it in System Settings. + MAC_CSC_LINK: ${{ runner.os == 'macOS' && secrets.MAC_CSC_LINK || '' }} + MAC_CSC_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_CSC_KEY_PASSWORD || '' }} + MAC_NOTARY_KEY: ${{ runner.os == 'macOS' && secrets.MAC_NOTARY_KEY || '' }} + MAC_NOTARY_KEY_ID: ${{ runner.os == 'macOS' && secrets.MAC_NOTARY_KEY_ID || '' }} + MAC_NOTARY_ISSUER: ${{ runner.os == 'macOS' && secrets.MAC_NOTARY_ISSUER || '' }} # The deb target needs a homepage, and electron-builder looks for .git only in electron/. Quoted for pwsh. - run: npx --no-install electron-builder --config electron-builder.yml --publish never "-c.extraMetadata.homepage=${{ github.server_url }}/${{ github.repository }}" + run: | + # electron-builder reads an empty CSC_LINK as a certificate to import, so these are exported only when set: + # a Developer ID certificate (base64 .p12), then an App Store Connect API key (.p8 text) to notarize with. + if [ -n "$MAC_CSC_LINK" ]; then + export CSC_LINK="$MAC_CSC_LINK" CSC_KEY_PASSWORD="$MAC_CSC_KEY_PASSWORD" CSC_IDENTITY_AUTO_DISCOVERY=true + if [ -n "$MAC_NOTARY_KEY" ]; then + printf '%s\n' "$MAC_NOTARY_KEY" > "$RUNNER_TEMP/notary-key.p8" + export APPLE_API_KEY="$RUNNER_TEMP/notary-key.p8" APPLE_API_KEY_ID="$MAC_NOTARY_KEY_ID" APPLE_API_ISSUER="$MAC_NOTARY_ISSUER" + fi + fi + npx --no-install electron-builder --config electron-builder.yml --publish never "-c.extraMetadata.homepage=${{ github.server_url }}/${{ github.repository }}" + - name: Release check inside the signed macOS app + if: runner.os == 'macOS' + # build.py checked the unsigned stage; this repeats it under the hardened runtime and entitlements users get. + run: python tools/release/build.py --check-app electron/release/mac-arm64/Riko.app - uses: actions/upload-artifact@v4 with: - name: Riko-${{ runner.os }}-x64 + name: Riko-${{ runner.os }}-${{ runner.os == 'macOS' && 'arm64' || 'x64' }} path: | electron/release/*.exe electron/release/*.AppImage electron/release/*.deb + electron/release/*.dmg if-no-files-found: error diff --git a/CLAUDE.md b/CLAUDE.md index baea63e..2428d79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -171,14 +171,14 @@ The data root is the directory holding `character_config.yaml`. ## Platform status -Releases ship CUDA and Vulkan bundles for Windows and Linux x64 only; nothing builds, packages, detects or tests Metal or ROCm. The Python inference layer is backend-agnostic: the backend is whatever llama.cpp build `runtime.native_library` loads, or whatever llama-server `runtime.provider: llama_server` talks to (any backend, without the emotion probe; checked against stock llama-server b11408 on Apple Silicon). The patched bridge, including the emotion probe, has been built and run on Apple Silicon Metal at the current pin. +Releases ship CUDA and Vulkan bundles for Windows and Linux x64 and a Metal bundle in an arm64 macOS DMG (macOS 14+, ad-hoc signed unless the MAC_* signing/notarization secrets exist); nothing builds, packages, detects or tests ROCm/HIP yet (one manifest entry plus a build container). The Python inference layer is backend-agnostic: the backend is whatever llama.cpp build `runtime.native_library` loads, or whatever llama-server `runtime.provider: llama_server` talks to (any backend, without the emotion probe; checked against stock llama-server b11408 on Apple Silicon). The patched bridge, including the emotion probe, has been built and run on Apple Silicon Metal at the current pin. Backend and device assumptions live here: - **Native build and packaging:** - - `tools/release/build.py`: only cuda/vulkan; collects only `.dll`/`.so` files; uses an `$ORIGIN` rpath, which macOS dyld ignores (macOS needs `@loader_path`). - - `.github/workflows/release.yml`: windows-2022 and ubuntu-22.04, CUDA 12.4.1. - - `electron/electron-builder.yml`: NSIS, AppImage and deb. -- **Bundled-backend allowlists and library names:** `app_core/configuration/config.py`, `electron/release.cjs` (detects GPUs via nvidia-smi and vulkaninfo) and `electron/src/first_setup.jsx`. They are asserted by `tests/test_release_config.py` and `electron/src/release.test.mjs`. + - `electron/native_backends.json` is the one manifest of backends (cuda, vulkan, metal), the platforms that ship each, per-OS library names (`riko-native.dll`, `libriko-native.so`, `libriko-native.dylib`), shared-library patterns, rpaths (`$ORIGIN`, `@loader_path`) and CMake switches. `app_core/configuration/native_backends.py`, `electron/native_backends.cjs`, `config.py` (`bundled:`), `release.cjs` (hardware detection runs only the current OS's detect commands: none on macOS) and `first_setup.jsx` read it; naming is tested with explicit per-platform tables on every host. + - `tools/release/build.py` builds the manifest's backends for the host OS, keeps versioned library symlinks, renders the macOS icon from `assets/logo.svg`, puts `Code/` on PyInstaller's path (without it `--collect-submodules process` silently froze none of Riko's dynamically imported modules) and repeats `--release-check` inside the packaged app (`--check-app`). + - `.github/workflows/release.yml`: windows-2022 and ubuntu-22.04 (CUDA 12.4.1, Vulkan) and macos-15 (Metal DMG). + - `electron/electron-builder.yml`: NSIS, AppImage, deb and a hardened-runtime mac dmg with `electron/build/entitlements.mac.plist` (allow-jit, audio-input, disable-library-validation, each needed by V8, the microphone or torch) and the `electron/build/mac_sign.cjs` signing hook. Packaged macOS asks for microphone access before starting the backend. - **Library loading:** `os.add_dll_directory` is called only on Windows (`app_core/inference/llama_native.py`, `Code/run_server.py`). - **NVIDIA-only telemetry:** `app_core/resources/gpu_memory.py` (nvidia-smi, DXGI vendor `0x10de`, PowerShell counters) and the CUDA rows in `app_core/resources/vram_estimate.py`. - **ASR:** every faster-whisper model is built by `app_core/audio/asr.py:create_whisper`. Defaults are `auto`/`default`, resolved from what CTranslate2 reports (CPU int8 on macOS, which has no CTranslate2 GPU backend); an unsupported configured pair falls back once with a warning, and Settings refuses one when it is edited. GPU ASR on Metal or ROCm needs a different engine (whisper.cpp, mlx-whisper, or a HIP-built CTranslate2). diff --git a/Code/process/app_core/configuration/config.py b/Code/process/app_core/configuration/config.py index 6b8973e..e2dc0e4 100644 --- a/Code/process/app_core/configuration/config.py +++ b/Code/process/app_core/configuration/config.py @@ -157,12 +157,8 @@ def load_config(path: str | Path | None = None) -> AppConfig: runtime_raw = raw.get("runtime", {}) library = runtime_raw.get('native_library') if isinstance(library, str) and library.startswith('bundled:'): - import sys - backend = library.split(':', 1)[1] - bundle = os.getenv('RIKO_BUNDLE_ROOT') - if backend not in {'cuda', 'vulkan'} or not bundle: - raise ValueError('Bundled native library requires the packaged app and CUDA or Vulkan backend') - library = Path(bundle) / 'native' / backend / ('riko-native.dll' if sys.platform == 'win32' else 'libriko-native.so') + from .native_backends import bundled_library # electron/native_backends.json: shipped backends, library names + library = bundled_library(library.split(':', 1)[1], os.getenv('RIKO_BUNDLE_ROOT')) runtime = RuntimeConfig( provider=runtime_raw.get("provider", "openai"), model=runtime_raw.get("model", raw.get("model", "")), diff --git a/Code/process/app_core/configuration/native_backends.py b/Code/process/app_core/configuration/native_backends.py new file mode 100644 index 0000000..7b1fb44 --- /dev/null +++ b/Code/process/app_core/configuration/native_backends.py @@ -0,0 +1,112 @@ +"""The native-backend manifest, electron/native_backends.json: which bundled llama.cpp builds ship for which OS, their +library file names and their build flags. Electron's first-run setup (electron/native_backends.cjs), the +`runtime.native_library: bundled:` resolution here and tools/release/build.py all read that one file; the +frozen backend carries a copy (build.py adds it with --add-data). Every function takes the platform (sys.platform +values), so tests check each OS on every host. Dependency-free: build.py loads this file by path.""" +from __future__ import annotations + +import functools +import json +import re +import sys +from pathlib import Path + +NAME = 'native_backends.json' +FROZEN_DIRECTORY = 'electron' # the manifest's folder inside the frozen backend, as in the repository +PLATFORMS = ('win32', 'linux', 'darwin') +_ID = re.compile(r'[a-z][a-z0-9_]*') # also the bundle's folder name: nothing that could leave resources/native +_SWITCH = re.compile(r'GGML_[A-Z0-9_]+') + + +def manifest_path() -> Path: + frozen = getattr(sys, '_MEIPASS', None) + return (Path(frozen) / FROZEN_DIRECTORY if frozen else Path(__file__).resolve().parents[4] / 'electron') / NAME + + +def validate(data: dict) -> dict: + """The manifest, or ValueError naming the first entry that would mis-resolve a bundle or a build.""" + def fail(message): raise ValueError(f'{NAME}: {message}') + platforms, backends = data.get('platforms'), data.get('backends') + if not isinstance(platforms, dict) or not platforms or not isinstance(backends, list) or not backends: + fail('needs a "platforms" object and a "backends" list') + for platform, entry in platforms.items(): + if platform not in PLATFORMS: fail(f'unknown platform {platform!r}') + library = entry.get('library') if isinstance(entry, dict) else None + if not isinstance(library, str) or not library or Path(library).name != library or '\\' in library: + fail(f'{platform}.library must be a bare file name') + patterns = entry.get('shared_libraries') + if not isinstance(patterns, list) or not patterns or not all(isinstance(item, str) and item for item in patterns): + fail(f'{platform}.shared_libraries must list file patterns') + if 'rpath' in entry and not isinstance(entry['rpath'], str): fail(f'{platform}.rpath must be a string') + seen, switches = set(), set() + for backend in backends: + name = backend.get('id') if isinstance(backend, dict) else None + if not isinstance(name, str) or not _ID.fullmatch(name): fail(f'backend id {name!r} must be lowercase letters, digits or _') + if name in seen: fail(f'backend {name!r} is listed twice') + seen.add(name) + if not all(isinstance(backend.get(key), str) and backend[key] for key in ('label', 'description')): + fail(f'{name} needs a label and a description') + shipped = backend.get('platforms') + if not isinstance(shipped, list) or not shipped or any(item not in platforms for item in shipped) or len(set(shipped)) != len(shipped): + fail(f'{name}.platforms must list platforms defined above') + switch = backend.get('cmake_switch') + if switch is not None and (not isinstance(switch, str) or not _SWITCH.fullmatch(switch) or switch in switches): + fail(f'{name}.cmake_switch must be a GGML_ option no other backend uses, or null for a CPU-only build') + switches.add(switch) + flags = backend.get('cmake_flags', []) + if not isinstance(flags, list) or not all(isinstance(flag, str) and flag.startswith('-D') for flag in flags): + fail(f'{name}.cmake_flags must be -D definitions') + detect = backend.get('detect') + if detect is not None and not (isinstance(detect, dict) and all(isinstance(detect.get(key), str) and detect[key] for key in ('command', 'label', 'missing')) + and isinstance(detect.get('args', []), list) and all(isinstance(item, str) for item in detect.get('args', [])) + and type(detect.get('required', False)) is bool): + fail(f'{name}.detect needs a command, args, label, missing text and a boolean required') + return data + + +@functools.cache +def manifest() -> dict: + return validate(json.loads(manifest_path().read_text(encoding='utf-8'))) + + +def _platform(platform): + return sys.platform if platform is None else platform + + +def platform_build(platform: str | None = None, data: dict | None = None) -> dict: + """{library, shared_libraries, rpath?} for a platform's bundles.""" + platform, platforms = _platform(platform), (data or manifest())['platforms'] + if platform not in platforms: raise ValueError(f'No native library is defined for platform {platform!r}') + return platforms[platform] + + +def library_name(platform: str | None = None, data: dict | None = None) -> str: + return platform_build(platform, data)['library'] + + +def backends_for(platform: str | None = None, data: dict | None = None) -> list[str]: + """Backend ids shipped for a platform, in preference order.""" + platform = _platform(platform) + return [backend['id'] for backend in (data or manifest())['backends'] if platform in backend['platforms']] + + +def bundled_library(backend: str, bundle: str | Path | None, platform: str | None = None, data: dict | None = None) -> Path: + """`bundled:` as a path inside the installed app (RIKO_BUNDLE_ROOT), so the configuration survives moving + or upgrading the app; only backends shipped for this platform, whose ids are safe folder names, resolve.""" + platform = _platform(platform) + shipped = backends_for(platform, data) + if not shipped: raise ValueError(f'Bundled native library: no bundled backend ships for {platform}') + if backend not in shipped: + raise ValueError(f'Bundled native library {backend!r} is not shipped for {platform}; use ' + + ' or '.join(f'bundled:{name}' for name in shipped)) + if not bundle: raise ValueError('Bundled native library requires the packaged app (RIKO_BUNDLE_ROOT is not set)') + return Path(bundle) / 'native' / backend / library_name(platform, data) + + +def cmake_flags(backend: str, data: dict | None = None) -> list[str]: + """The backend's own switch ON, every other backend's OFF (macOS turns Metal on by default), then its flags.""" + data = data or manifest() + entry = next((item for item in data['backends'] if item['id'] == backend), None) + if entry is None: raise ValueError(f'Unknown native backend {backend!r}') + switches = [item['cmake_switch'] for item in data['backends'] if item.get('cmake_switch')] + return [f'-D{switch}={"ON" if switch == entry.get("cmake_switch") else "OFF"}' for switch in switches] + list(entry.get('cmake_flags', [])) diff --git a/Code/run_server.py b/Code/run_server.py index d0184e5..7e269aa 100644 --- a/Code/run_server.py +++ b/Code/run_server.py @@ -77,6 +77,10 @@ def release_check(library=None): import importlib import subprocess import tempfile + from process.app_core.configuration.native_backends import backends_for, library_name + shipped, name = backends_for(sys.platform), library_name(sys.platform) # the frozen copy bundled: resolves with + if library and (Path(library).name != name or Path(library).absolute().parent.name not in shipped): + raise SystemExit(f'{library} is not where bundled: looks on {sys.platform}: native/<{"|".join(shipped)}>/{name}') if library: library = Path(library).resolve() directory = os.add_dll_directory(str(library.parent)) if os.name == 'nt' else None diff --git a/electron/build/entitlements.mac.plist b/electron/build/entitlements.mac.plist new file mode 100644 index 0000000..db7622b --- /dev/null +++ b/electron/build/entitlements.mac.plist @@ -0,0 +1,17 @@ + + + + + + + com.apple.security.cs.allow-jit + + + com.apple.security.device.audio-input + + + com.apple.security.cs.disable-library-validation + + + diff --git a/electron/build/mac_sign.cjs b/electron/build/mac_sign.cjs new file mode 100644 index 0000000..f32a8af --- /dev/null +++ b/electron/build/mac_sign.cjs @@ -0,0 +1,22 @@ +// electron-builder's mac.sign hook (electron-builder.yml). electron-builder 26.0.12 signs only with a keychain identity +// (CSC_LINK, or a Developer ID it discovers) and otherwise skips signing: the app would keep Electron's stock signature, +// broken by the renamed executable and Info.plist, with no hardened runtime and none of entitlements.mac.plist. This +// signs with the identity electron-builder found, or ad hoc ('-') without one, through the same @electron/osx-sign call and +// per-file options (hardened runtime, entitlements) electron-builder would use. Notarization still needs a real identity. +const path=require('path'); +function signOptions(options){return {...options,identity:options.identity||'-',identityValidation:false};} +// osx-sign comes with electron-builder (app-builder-lib's pinned dependency), so resolve it from there. +function osxSign(){return require(require.resolve('@electron/osx-sign',{paths:[path.dirname(require.resolve('app-builder-lib/package.json'))]}));} +// electron-builder calls sign(options, packager); the packager is not used. Like electron-builder's own signing, retry: with +// a real identity every Mach-O gets a timestamp from Apple's server, which sometimes drops a request. +const retry={attempts:3,delay:5000}; +async function sign(options){ + for(let attempt=1;;attempt++){ + try{return await osxSign().signAsync(signOptions(options));} + catch(error){if(attempt>=retry.attempts)throw error;await new Promise(resolve=>setTimeout(resolve,retry.delay));} + } +} +module.exports=sign; +module.exports.retry=retry; +module.exports.signOptions=signOptions; +module.exports.osxSign=osxSign; diff --git a/electron/electron-builder.yml b/electron/electron-builder.yml index 2a5534a..35477a4 100644 --- a/electron/electron-builder.yml +++ b/electron/electron-builder.yml @@ -5,6 +5,7 @@ directories: output: release files: - '*.cjs' + - native_backends.json # electron/native_backends.cjs requires it - dist/** - package.json extraResources: @@ -35,6 +36,26 @@ linux: maintainer: Riko contributors # The overlay needs window positioning, always-on-top and pointer forwarding, which native Wayland ignores; run under XWayland. executableArgs: ['--ozone-platform=x11'] # deb launcher +# A granted microphone (macOS TCC) must not be usable by other local processes that relaunch Riko as plain Node. +electronFuses: + runAsNode: false + enableNodeOptionsEnvironmentVariable: false + enableNodeCliInspectArguments: false +mac: + target: + - target: dmg + arch: [arm64] + category: public.app-category.utilities + icon: ../release-stage/mac/icon.icns # tools/release/build.py renders it from assets/logo.svg + # The frozen backend's locked wheels (torch, numpy, scipy, onnxruntime, av) need macOS 14; the Metal bundle targets it too. + minimumSystemVersion: '14.0' + hardenedRuntime: true + entitlements: build/entitlements.mac.plist + entitlementsInherit: build/entitlements.mac.plist + # Signs ad hoc unless a Developer ID certificate is configured (CSC_LINK); notarizes only with APPLE_* credentials. + sign: ./build/mac_sign.cjs + extendInfo: + NSMicrophoneUsageDescription: Riko listens for its wake word and your voice after you turn the microphone on. appImage: # Replaces electron-builder's default '--no-sandbox', which Ubuntu 24.04+ needs for AppImages, so keep it. executableArgs: ['--no-sandbox', '--ozone-platform=x11'] diff --git a/electron/main.cjs b/electron/main.cjs index 4fc3eee..9d33d0d 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1,4 +1,4 @@ -const { app, BrowserWindow, globalShortcut, ipcMain, dialog, screen, session, shell, Tray, Menu, nativeImage } = require('electron'); +const { app, BrowserWindow, globalShortcut, ipcMain, dialog, screen, session, shell, Tray, Menu, nativeImage, systemPreferences } = require('electron'); // Hardware acceleration is Electron's default; keep WebGL and GPU rasterization enabled. app.commandLine.appendSwitch('enable-gpu-rasterization'); // Linux Wayland: the overlay needs X11 (placement, always-on-top, click-through) and Ozone is chosen before this file @@ -16,7 +16,7 @@ app.on('second-instance',()=>{if(setupWindow&&!setupWindow.isDestroyed())setupWi const release=require('./release.cjs'); const {shortcutBindings}=require('./shortcuts.cjs'); function setupSender(event){if(!setupWindow||setupWindow.isDestroyed()||event.sender.id!==setupWindow.webContents.id)throw new Error('Setup window required');} -ipcMain.handle('setup-hardware',async event=>{setupSender(event);const hw=await release.hardware();try{hw.gpus=(await app.getGPUInfo('basic')).gpuDevice?.map(device=>device.deviceString||`GPU vendor ${device.vendorId}, device ${device.deviceId}`)||[];hw.vulkan=hw.gpus.join('\n')+'\n'+hw.vulkan;}catch{}return hw;}); +ipcMain.handle('setup-hardware',async event=>{setupSender(event);const hw=await release.hardware();try{hw.gpus=(await app.getGPUInfo('basic')).gpuDevice?.map(device=>device.deviceString||`GPU vendor ${device.vendorId}, device ${device.deviceId}`)||[];}catch{hw.gpus=[];}return hw;}); ipcMain.handle('setup-directory',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openDirectory','createDirectory']});return result.canceled?null:result.filePaths[0];}); ipcMain.handle('setup-model',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openFile'],filters:[{name:'GGUF models',extensions:['gguf']}]});return result.canceled?null:result.filePaths[0];}); ipcMain.handle('setup-sovits',async event=>{setupSender(event);const result=await dialog.showOpenDialog(setupWindow,{properties:['openFile']});return result.canceled?null:result.filePaths[0];}); @@ -196,6 +196,16 @@ function setBoardMode(mode,instant=false){ whiteboard.setAlwaysOnTop(true);whiteboard.show();whiteboard.focus(); } function showControls(view,mode=controlMode,anchor){if(view&&view!=='chat')mode='full';else if(mode==='collapsed')mode='compact';setControlMode(mode,anchor);if(control.isMinimized())control.restore();control.show();control.focus();if(view)control.webContents.send('navigate',view);} +// macOS hands a process without microphone access silent buffers, not an error, so say so once (until access is granted). +function microphoneNotice(blocked){ + const marker=path.join(app.getPath('userData'),'microphone-blocked-notice'); + if(!blocked){fs.rmSync(marker,{force:true});return;} + if(fs.existsSync(marker))return; + try{fs.writeFileSync(marker,'');}catch{} + dialog.showMessageBox({type:'info',buttons:['Open Settings','Not now'],defaultId:0,cancelId:1,message:'Riko cannot hear you', + detail:'Microphone access for Riko is off, so voice input records silence. Turn it on in System Settings → Privacy & Security → Microphone.'}) + .then(({response})=>{if(response===0)shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');}).catch(()=>{}); +} function createTray(characterName){ const icon=nativeImage.createFromPath(brandIcon); if(icon.isEmpty())throw new Error('Tray icon is missing or invalid: assets/tray.png'); @@ -216,6 +226,8 @@ app.whenReady().then(async () => { process.env.RIKO_CONFIG=path.join(root,'character_config.yaml'); const crypto=require('crypto'); ownSecrets={api_token:crypto.randomBytes(32).toString('base64url'),confirm_key:crypto.randomBytes(32).toString('base64url'),listening:false}; + // macOS attributes the backend's microphone use to this app, so it asks now; the answer does not hold up the model load. + release.askMicrophone(systemPreferences).then(state=>microphoneNotice(state==='blocked')).catch(()=>{}); // Nothing carries the token until the backend says it holds the port, and nothing does after it exits. backendProcess=release.startBackend(root,process.resourcesPath,ownSecrets,()=>{ownSecrets.listening=true;publishProcesses(true);}); backendProcess.on('error',error=>dialog.showErrorBox('Backend failed to launch',error.message)); diff --git a/electron/native_backends.cjs b/electron/native_backends.cjs new file mode 100644 index 0000000..26c96b7 --- /dev/null +++ b/electron/native_backends.cjs @@ -0,0 +1,15 @@ +// The native-backend manifest (native_backends.json) names the bundled llama.cpp builds each OS ships, their library +// file names and build flags; the Python backend (Code/process/app_core/configuration/native_backends.py) and +// tools/release/build.py read the same file. Every function takes the platform, so tests cover each OS on any host. +const path=require('path'); +const MANIFEST=require('./native_backends.json'); +function libraryName(platform=process.platform,manifest=MANIFEST){if(!Object.hasOwn(manifest.platforms,platform))throw new Error('No native library is defined for '+platform);return manifest.platforms[platform].library;} +// Shipped for the platform, in preference order. +function backendsFor(platform=process.platform,manifest=MANIFEST){return manifest.backends.filter(backend=>backend.platforms.includes(platform));} +function nativeLibrary(resources,backend,platform=process.platform,manifest=MANIFEST){ + if(!backendsFor(platform,manifest).some(item=>item.id===backend))throw new Error('No '+backend+' native backend ships for '+platform); + return path.join(resources,'native',backend,libraryName(platform,manifest)); +} +// detected: {backend id: detection output}. The first shipped backend that needs no detection or whose detection found hardware. +function preferredBackend(platform=process.platform,detected={},manifest=MANIFEST){return backendsFor(platform,manifest).find(backend=>!backend.detect?.required||detected[backend.id])?.id||null;} +module.exports={MANIFEST,libraryName,backendsFor,nativeLibrary,preferredBackend}; diff --git a/electron/native_backends.json b/electron/native_backends.json new file mode 100644 index 0000000..1c967ec --- /dev/null +++ b/electron/native_backends.json @@ -0,0 +1,50 @@ +{ + "about": "The native llama.cpp bundles a packaged app can ship, as resources/native//. Read by electron/native_backends.cjs (first-run setup), Code/process/app_core/configuration/native_backends.py (runtime.native_library: bundled:; the frozen backend carries a copy) and tools/release/build.py. Backends are in preference order: setup preselects the first one shipped for the OS whose required detection, if any, found hardware. A build turns its backend's cmake_switch ON and every other backend's OFF, then adds cmake_flags.", + "platforms": { + "win32": {"library": "riko-native.dll", "shared_libraries": ["*.dll"]}, + "linux": {"library": "libriko-native.so", "shared_libraries": ["*.so", "*.so.*"], "rpath": "$ORIGIN"}, + "darwin": {"library": "libriko-native.dylib", "shared_libraries": ["*.dylib"], "rpath": "@loader_path"} + }, + "backends": [ + { + "id": "cuda", + "label": "CUDA", + "description": "preferred for NVIDIA", + "platforms": ["win32", "linux"], + "cmake_switch": "GGML_CUDA", + "cmake_flags": ["-DCMAKE_CUDA_FLAGS=--diag-suppress=221", "-DGGML_CUDA_NO_VMM=ON"], + "notes": "Upstream uses -INFINITY as a max-reduction sentinel and MSVC's expansion triggers NVCC #221 repeatedly, so it is suppressed and the upstream code kept. Without GGML_CUDA_NO_VMM, ggml-cuda links the driver library (nvcuda.dll, libcuda.so.1) directly, so the bundle could not load at all without an NVIDIA driver, as on CI runners; cudart still loads the driver when CUDA starts. The cost: the CUDA memory pool grows with cudaMalloc instead of virtual-memory mappings.", + "detect": { + "command": "nvidia-smi", + "args": ["--query-gpu=name,memory.total,driver_version", "--format=csv,noheader,nounits"], + "label": "NVIDIA GPU (nvidia-smi)", + "missing": "No NVIDIA GPU detected via nvidia-smi", + "required": true + } + }, + { + "id": "vulkan", + "label": "Vulkan", + "description": "AMD/Intel/NVIDIA", + "platforms": ["win32", "linux"], + "cmake_switch": "GGML_VULKAN", + "cmake_flags": [], + "detect": { + "command": "vulkaninfo", + "args": ["--summary"], + "label": "Vulkan detection", + "missing": "Vulkan enumeration unavailable; driver support must be checked", + "required": false + } + }, + { + "id": "metal", + "label": "Metal", + "description": "Apple silicon GPU", + "platforms": ["darwin"], + "cmake_switch": "GGML_METAL", + "cmake_flags": ["-DGGML_METAL_EMBED_LIBRARY=ON", "-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0"], + "notes": "The embedded shader library needs no Metal toolchain or default.metallib at run time (built and run on Apple silicon at the current llama.cpp pin). macOS 14.0 is the oldest the frozen backend runs on (the locked torch, numpy, scipy, onnxruntime and av wheels), so the library targets it rather than the build host's macOS; electron-builder.yml's mac.minimumSystemVersion matches." + } + ] +} diff --git a/electron/release.cjs b/electron/release.cjs index 00acdd9..38faef5 100644 --- a/electron/release.cjs +++ b/electron/release.cjs @@ -3,18 +3,24 @@ const fs=require('fs'); const os=require('os'); const {execFile,spawn}=require('child_process'); const YAML=require('yaml'); +const native=require('./native_backends.cjs'); const run=(file,args)=>new Promise(resolve=>execFile(file,args,{timeout:5000,windowsHide:true},(error,stdout)=>resolve(error?'':stdout.trim()))); -async function hardware(){ - const gpu=await run('nvidia-smi',['--query-gpu=name,memory.total,driver_version','--format=csv,noheader,nounits']); - const vulkan=await run('vulkaninfo',['--summary']); - return {cpu:os.cpus()[0]?.model||'Unknown',threads:os.cpus().length,ramGB:Math.round(os.totalmem()/2**30),nvidia:gpu,vulkan:vulkan||'Vulkan enumeration unavailable; driver support must be checked',preferred:gpu?'cuda':'vulkan'}; +// Runs only the detection tools of this OS's bundles (macOS ships Metal and has neither nvidia-smi nor vulkaninfo). +async function hardware(platform=process.platform,probe=run){ + const backends=native.backendsFor(platform),probed=backends.filter(backend=>backend.detect); + const outputs=await Promise.all(probed.map(backend=>probe(backend.detect.command,backend.detect.args))); + const detected=Object.fromEntries(probed.map((backend,index)=>[backend.id,outputs[index]])); + return {cpu:os.cpus()[0]?.model||'Unknown',threads:os.cpus().length,ramGB:Math.round(os.totalmem()/2**30),platform, + backends:backends.map(({id,label,description})=>({id,label,description})), + detections:probed.map(backend=>({backend:backend.id,label:backend.detect.label,required:!!backend.detect.required,output:detected[backend.id]||backend.detect.missing})), + preferred:native.preferredBackend(platform,detected)}; } -function nativeLibrary(resources,backend){return path.join(resources,'native',backend,process.platform==='win32'?'riko-native.dll':'libriko-native.so');} -function configuration(input,resources){ +function configuration(input,resources,platform=process.platform){ if(input.sovitsAuto&&(!path.isAbsolute(input.sovitsExecutable||'')||!fs.existsSync(input.sovitsExecutable)))throw new Error('Choose an existing absolute GPT-SoVITS executable'); - if(!['cuda','vulkan'].includes(input.backend))throw new Error('Choose CUDA or Vulkan'); - const library=nativeLibrary(resources,input.backend); + const shipped=native.backendsFor(platform); + if(!shipped.some(backend=>backend.id===input.backend))throw new Error(shipped.length?'Choose '+shipped.map(backend=>backend.label).join(' or '):'No native backend ships for '+platform); + const library=native.nativeLibrary(resources,input.backend,platform); if(!fs.existsSync(library))throw new Error('Packaged native backend is missing: '+library); const context=Number(input.context),output=Number(input.output),threads=Number(input.threads); if(!Number.isInteger(context)||context<2048||context>131072||!Number.isInteger(output)||output<64||output>=context)throw new Error('Invalid context/output budget'); @@ -31,10 +37,10 @@ function configuration(input,resources){ // macOS, the executable's folder elsewhere. function installRoot(executable,platform=process.platform){return platform==='darwin'?path.resolve(executable,'..','..','..'):path.dirname(executable);} function inside(parent,child){const relative=path.relative(path.resolve(parent),path.resolve(child));return relative===''||relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative);} -function saveSetup(directory,input,resources,install=resources){ +function saveSetup(directory,input,resources,install=resources,platform=process.platform){ if(!path.isAbsolute(directory))throw new Error('Choose an absolute data directory'); if([install,resources].some(root=>inside(root,directory)))throw new Error('Choose a data folder outside the application folder ('+path.resolve(install)+'); updates and uninstall delete everything inside it'); - const config=configuration(input,resources); + const config=configuration(input,resources,platform); fs.mkdirSync(directory,{recursive:true}); for(const folder of ['models','persistent_memories','logs'])fs.mkdirSync(path.join(directory,folder),{recursive:true}); // Never overwrite an existing user configuration, even after a failed first run. @@ -56,6 +62,17 @@ function watchListening(onListening){ // npx, uvx and ffmpeg from MCP servers and the Discord worker; put its prefixes first, as a Terminal does. const HOMEBREW_PATHS=['/opt/homebrew/bin','/opt/homebrew/sbin','/usr/local/bin']; function finderPath(current){const parts=String(current||'/usr/bin:/bin:/usr/sbin:/sbin').split(':').filter(Boolean);return [...HOMEBREW_PATHS.filter(dir=>!parts.includes(dir)),...parts].join(':');} +// macOS attributes the packaged backend's microphone use to this app: the hardened runtime needs the audio-input +// entitlement, and the prompt shows NSMicrophoneUsageDescription. Ask before the backend starts, so the prompt names Riko +// at launch instead of interrupting the first voice turn. Resolves true/false once answered, null when nothing was asked. +// 'blocked' when macOS already denies the microphone: the backend's input then opens but records silence. +function askMicrophone(preferences,platform=process.platform){ + if(platform!=='darwin')return Promise.resolve(null); + const status=preferences.getMediaAccessStatus('microphone'); + if(status==='denied'||status==='restricted')return Promise.resolve('blocked'); + if(status!=='not-determined')return Promise.resolve(null); + return Promise.resolve().then(()=>preferences.askForMediaAccess('microphone')).catch(()=>false); +} function startBackend(directory,resources,secrets={},onListening=()=>{}){ const executable=path.join(resources,'backend',process.platform==='win32'?'riko-backend.exe':'riko-backend'); const logPath=path.join(directory,'logs','backend-launch.log'); @@ -80,4 +97,4 @@ function startSovits(settings){ } // Ask first; the backend gives itself 14 s. uvicorn ignores SIGTERM while already shutting down, so escalate to SIGKILL. function stopBackend(child,grace=15000,force=3000){return new Promise(resolve=>{if(!child||child.exitCode!==null||child.signalCode!==null){resolve();return;}let timer=setTimeout(()=>{child.kill();timer=setTimeout(()=>{child.kill('SIGKILL');resolve();},force);},grace);child.once('exit',()=>{clearTimeout(timer);resolve();});child.stdin.end('shutdown\n');});} -module.exports={hardware,configuration,saveSetup,installRoot,startBackend,nativeLibrary,startSovits,stopBackend,watchListening,finderPath,LISTENING}; +module.exports={hardware,configuration,saveSetup,installRoot,askMicrophone,startBackend,nativeLibrary:native.nativeLibrary,startSovits,stopBackend,watchListening,finderPath,LISTENING}; diff --git a/electron/src/first_setup.jsx b/electron/src/first_setup.jsx index 8711475..bd33c05 100644 --- a/electron/src/first_setup.jsx +++ b/electron/src/first_setup.jsx @@ -1,10 +1,10 @@ import React,{useEffect,useState} from 'react'; export default function FirstSetup(){ const [hw,setHw]=useState(null),[directory,setDirectory]=useState(''),[error,setError]=useState(''),[busy,setBusy]=useState(false); - const [form,setForm]=useState({backend:'vulkan',repo:'',filename:'',revision:'main',modelPath:'',context:8192,output:1024,threads:4,name:'Riko',prompt:'You are a helpful local companion.',memories:'',julia:true,embeddings:true,reflection:false,cpuOnly:false,sovitsExecutable:'',sovitsAuto:false,sovitsUrl:'http://127.0.0.1:9880/tts',referenceAudio:'',referenceText:''}); + const [form,setForm]=useState({backend:'',repo:'',filename:'',revision:'main',modelPath:'',context:8192,output:1024,threads:4,name:'Riko',prompt:'You are a helpful local companion.',memories:'',julia:true,embeddings:true,reflection:false,cpuOnly:false,sovitsExecutable:'',sovitsAuto:false,sovitsUrl:'http://127.0.0.1:9880/tts',referenceAudio:'',referenceText:''}); useEffect(()=>{window.setupBridge.hardware().then(h=>{setHw(h);setForm(f=>({...f,backend:h.preferred,threads:Math.max(1,Math.floor(h.threads/2))}));}).catch(e=>setError(e.message));},[]); const field=(key,label,type='text')=>; async function finish(){setBusy(true);setError('');try{await window.setupBridge.finish({directory,settings:form});}catch(e){setError(e.message);setBusy(false);}} const speech=

Optional GPT-SoVITS startup

Choose an API-server executable, not a GUI launcher. It must expose the configured TTS endpoint. Install its models and dependencies separately. Automatic startup is opt-in and runs with your user permissions.

{form.sovitsExecutable||'None selected'}

{field('sovitsUrl','TTS endpoint URL')}{field('referenceAudio','Reference audio path (as seen by the TTS server)')}{field('referenceText','Reference transcript')}

Executable arguments are configurable later in Settings. Startup does not guarantee service readiness; speech will report connection errors if the service is still loading.

; - return

Welcome — set up your companion

No model, microphone or background memory processing starts until you confirm.

{hw&&

Your hardware

{hw.cpu} · {hw.threads} logical threads · {hw.ramGB} GB RAM

{hw.nvidia||'No NVIDIA GPU detected via nvidia-smi'}
Vulkan detection
{hw.vulkan}
}

Private data folder

Models, training examples, memories, history and logs stay here across upgrades. Choose a writable folder with plenty of free space. Existing files are never overwritten.

{directory||'Not selected'}

Model and inference

Select a GGUF you already own, or enter an exact Hugging Face repository and filename. A commit revision is recommended. Model licenses and download size are your responsibility; confirmation permits the download.

{form.modelPath}

{form.modelPath&&}{!form.modelPath&&<>{field('repo','Repository (owner/model)')}{field('filename','Exact GGUF filename')}{field('revision','Model revision')}}

Compatible GPU drivers are required. Detection is advisory, not a guarantee. A failed CUDA load does not silently change your configuration; switch to Vulkan in Settings.

{field('context','Context length (prompt + reply tokens)','number')}{field('output','Maximum reply tokens','number')}{field('threads','CPU threads','number')}

Larger context uses more VRAM/RAM, not necessarily better answers. Start with 4096–8192 tokens and leave GPU room for audio and graphics. Two request slots and conservative f16 caches are used initially.

{['cpuOnly','julia','embeddings','reflection'].map(key=>)}

Personality and seed memories

{field('name','Companion name')}