diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..befa632 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +name: check + +on: + push: + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + python-version: "3.12" + - run: sudo apt-get install -y libportaudio2 + - run: uv lock --check + - run: uvx ruff check v2t tests + - name: Mechanics smoke tests + run: >- + uv run --no-project --python 3.12 + --with 'numpy<2.4' --with scipy --with sounddevice --with loguru + python -m unittest -v tests.test_smoke + - run: uv build + - run: uvx twine check dist/* + + native: + runs-on: macos-latest + steps: + - uses: actions/checkout@v5 + - run: xcrun swiftc -typecheck -parse-as-library v2t/native/Voice2Text.swift -framework AppKit -framework AVFoundation -framework ApplicationServices diff --git a/.gitignore b/.gitignore index 2d43501..c8d0c3e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ dist/ !.pixi/config.toml .env + +# Local agent handoff state +.continues-handoff.md +.omo/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 889c9bf..c3fcd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.3.0 + +The single-file proof-of-concept is preserved at the [`nano`](https://github.com/lucharo/voice2text/releases/tag/nano) tag. +This release turns v2t into a small, modular, MLX-first package. + +- **Parakeet is the default backend** (`mlx-community/parakeet-tdt-0.6b-v3`) — ~10× faster than Whisper on Apple Silicon, multilingual. Ships in core, so `uv tool install voice2text` just works. +- **Pluggable STT backends** — Whisper is an optional alternative: `uv tool install 'voice2text[whisper]'`, then select it in config. +- **Pluggable cleanup engines** — default is **mlx-lm, in-process** (`Qwen2.5-1.5B-Instruct`): no Ollama, no daemon, same MLX stack as transcription. Ollama stays as an optional `engine = "ollama"`. Use a non-thinking model with either. +- **Lower idle memory and faster cleanup** — the default cleanup model drops from 4B to the locally validated 1.5B model, and paste timing now appears in logs/history. +- **Native menu icon** — the menu uses adaptive SF Symbols instead of emoji for off, loading, ready, recording, processing, and error states. +- **Guided `v2t setup`** — pick the transcription model and cleanup engine; detects Ollama and offers it, otherwise defaults to mlx-lm. Writes `~/.v2t/config.toml`. +- **Config file** at `~/.v2t/config.toml` (honors `$V2T_HOME` / `$XDG_CONFIG_HOME`), with `v2t config [--init]`. +- **Transcription history** — every result + metadata appended to `~/.v2t/history/transcriptions.jsonl`. +- **Optional one-file menu-bar app** — native permission identity, immediate state, Start/Stop, and config/history/log links without a window or Xcode project. +- **Optional LaunchAgent** — `v2t service install` starts the same menu app at login and keeps one warm Python engine. +- **Reliable runtime lifecycle** — single-instance locking, honest live states, microphone failure recovery, native permission checks, and safe shutdown while a transcription finishes. +- **Private, lossless local data** — config/history/status use owner-only permissions, temporary audio is always removed, logs no longer include dictated text, and rich clipboard contents survive paste. +- **Reproducible install and release path** — constrained scientific dependencies, bundled Swift source, clean sdists, smoke-test CI, and validated build/release commands. +- **Contributor benchmark harness** — `just bench` writes a per-machine markdown grid (STT RTF + cleanup TTFT/total) with `engine:model` columns. Optional Whisper stays in the dev dependency group. +- **New commands**: `v2t setup`, `v2t status`, `v2t stop`, `v2t service`, `v2t config`, `v2t menubar`. + ## 0.2.0 - Warm up both Whisper and Ollama models at startup so the first transcription is fast diff --git a/README.md b/README.md index 0027961..2007568 100644 --- a/README.md +++ b/README.md @@ -3,116 +3,188 @@ [![PyPI](https://img.shields.io/pypi/v/voice2text)](https://pypi.org/project/voice2text/) [![Downloads](https://static.pepy.tech/badge/voice2text/month)](https://pepy.tech/project/voice2text) [![Total Downloads](https://static.pepy.tech/badge/voice2text)](https://pepy.tech/project/voice2text) -[![macOS](https://img.shields.io/badge/macOS-only-blue?logo=apple)](https://github.com/lucharo/voice2text) +[![macOS](https://img.shields.io/badge/macOS-Apple%20Silicon-blue?logo=apple)](https://github.com/lucharo/voice2text) [![Works on my machine](https://img.shields.io/badge/works-on%20my%20machine-brightgreen)](https://github.com/lucharo/voice2text) -Local voice-to-text with Whisper + LLM cleanup. Push-to-talk (Right ⌘), pastes at cursor. +Local, MLX-first voice-to-text. Hold **Right ⌘**, talk, release — it transcribes with +[Parakeet](https://huggingface.co/mlx-community/parakeet-tdt-0.6b-v3), cleans the text up with a +small local LLM, and pastes at your cursor. No cloud or account; after the one-time model download, +it runs without a network connection. -Voice-to-text tools like [Wispr Flow](https://wisprflow.ai/), [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper), and [VoiceInk](https://www.voiceink.app/) are becoming increasingly popular. It's a testament to our times that in 2025, ~270 lines of Python with local Whisper and a small `ollama` language model (Qwen 2.5-3B) can deliver a comparable experience on consumer hardware. Such tooling would have been unimaginable 3 years ago. This project is a proof of concept to demonstrate just that. +Voice-to-text tools like [Wispr Flow](https://wisprflow.ai/), [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper), +and [VoiceInk](https://www.voiceink.app/) are great until the network gets in the way — Wispr Flow +[isn't compatible with most VPNs](https://docs.wisprflow.ai/troubleshooting), so behind a corporate +SSL-intercepting gateway (Zscaler and friends) it stalls or fails. Once the models are cached, going +local makes that whole class of problem disappear: the VPN is irrelevant. Speech models are good +enough now that the basics fit in a small Python package on consumer hardware. -> **Note:** Before anyone suggests splitting this into modules and submodules — this is an intentional design choice to demonstrate how this whole functionality fits in less than 300 lines of python code. +> **Heritage:** this started as a single `voice2text.py` under 300 lines — that proof-of-concept is +> preserved forever at the [`nano`](https://github.com/lucharo/voice2text/releases/tag/nano) tag. +> From `0.3.0` it's a small, modular package: pluggable MLX backends, one config file, a benchmark +> harness, and an optional one-file menu-bar app. -> **Note 2:** This is macOS-only by design. We use: -> - **[mlx-whisper](https://github.com/ml-explore/mlx-examples/tree/main/whisper)** — optimized for Apple Silicon -> - **osascript** — for simulating Cmd+V paste via System Events -> - **pbcopy/pbpaste** — macOS clipboard -> - **[nowplaying-cli](https://github.com/kirtan-shah/nowplaying-cli)** — macOS media control -> - **System Preferences** URLs for permissions -> -> You're welcome to fork this and make it work on Linux or Windows! +> **Design tenet — be communicative.** An ergonomic tool is an expressive one: every action gets +> immediate, visible feedback. The optional menu-bar icon tracks live state (off · loading · ready · +> recording · transcribing · cleaning · error), so you always know whether the tool heard you. -## Prerequisites +## What you get -> Skip this if using **`pixi`** — it handles ollama automatically. - -```bash -brew install ollama -ollama pull qwen2.5:3b -``` +- **Push-to-talk** — hold Right ⌘ (configurable), release to transcribe + paste. +- **Parakeet (MLX)** transcription by default — ~10× faster than Whisper on Apple Silicon, English + 24 European languages. Whisper stays available as a fallback for rare languages/accents. +- **In-process LLM cleanup** via mlx-lm (`Qwen2.5-1.5B-Instruct`) — fixes punctuation, removes fillers while using about 1.3 GB less model memory than the previous 4B default. No Ollama, no daemon. Strict or casual. (Ollama optional — see [Cleanup engine](#cleanup-engine).) +- **Pastes at cursor**, restoring your previous clipboard. +- **One config file** at `~/.v2t/config.toml`, plus a JSONL **history** of every transcription. +- **Optional one-file Swift menu** — native permissions, instant state, and quick links; no window or Xcode project. ## Install -### uvx (quick try) - -The fastest way to try it out. Note: startup is slower because `uvx` creates a fresh virtual environment each time. +Requires **macOS on Apple Silicon** (MLX). Parakeet transcription and in-process mlx-lm cleanup ship +as dependencies, with no daemon. The models download from Hugging Face on first launch, then remain +in the local cache: ```bash -uvx --from voice2text v2t +uv tool install voice2text # Parakeet STT + in-process Qwen2.5 cleanup +v2t setup # optional: pick models, detect Ollama, write config +v2t ``` -Or from GitHub: +Prefer Whisper for transcription? Add the extra (quote the brackets — zsh treats them as globs): ```bash -uvx --from git+https://github.com/lucharo/voice2text v2t +uv tool install 'voice2text[whisper]' # adds the Whisper backend; select it in config ``` -### uv tool install (recommended for daily use) - -Installs `v2t` as a persistent command — no virtual environment setup on each run, so startup is fast. +
+Other install methods (uvx, pip, dev) ```bash -uv tool install voice2text -v2t -``` +# quick try (fresh venv each run — slower startup) +uvx --from voice2text v2t -### pip +# pip +pip install voice2text && v2t -```bash -pip install voice2text -v2t +# from source +git clone https://github.com/lucharo/voice2text.git && cd voice2text +uv sync --no-dev && uv run v2t ``` +
-### Development install +## Usage ```bash -git clone https://github.com/lucharo/voice2text.git -cd voice2text -uv sync -uv run v2t +v2t # run push-to-talk (strict cleanup, parakeet) +v2t --casual # light cleanup (punctuation + fillers only) +v2t --no-cleanup # paste raw transcription, skip the LLM +v2t --backend whisper # use the whisper backend for this run +v2t --pause-music # pause media while recording (needs nowplaying-cli) + +v2t setup # guided config: pick models, detect Ollama +v2t status # running / idle (also used by the menu app) +v2t stop # stop a running v2t gracefully (--force if it is stuck) +v2t config # show resolved config + paths (--init writes a template) +v2t menubar install # optional: compile + open the tiny native menu app +v2t service install # optional: start that menu app at login ``` -### Pixi +Hold **Right Command** to record, release to transcribe and paste. -Pixi handles the ollama dependency automatically: +### Strict vs Casual -```bash -git clone https://github.com/lucharo/voice2text.git -cd voice2text -pixi run ollama pull qwen2.5:3b -pixi run v2t +| Raw transcription | Strict | Casual | +|-------------------|--------|--------| +| "Hey um I'll see you tomorrow at 9 actually no make it 10" | "Hey, I'll see you tomorrow at 10." | "Hey, I'll see you tomorrow at 9, actually no, make it 10." | +| "So basically I was thinking we could um you know maybe try the other approach" | "I was thinking we could try the other approach." | "So basically, I was thinking we could maybe try the other approach." | + +**Strict** (default) removes fillers, restructures for clarity. **Casual** only adds punctuation and removes "um/uh", keeping your phrasing. + +## Config — `~/.v2t/` + +Everything lives in one directory (override with `$V2T_HOME`, or `$XDG_CONFIG_HOME/v2t`): + +``` +~/.v2t/ + config.toml # all settings (v2t config --init to create) + history/transcriptions.jsonl # every transcription + metadata (toggle in config) + run/ # private runtime status + log ``` -> **Note:** We don't publish to conda-forge/pixi channels yet, but may in the future. +`config.toml` (every key optional — these are the defaults): -## Usage +```toml +[transcription] +backend = "parakeet" # parakeet (MLX) | whisper +model = "" # blank = backend default -```bash -v2t # strict mode (restructures sentences) -v2t --casual # light cleanup (punctuation only) -v2t --pause-music # pause media while recording (macOS only, requires nowplaying-cli via brew) +[cleanup] +enabled = true +engine = "mlx" # mlx (in-process via mlx-lm) | ollama +model = "" # blank = engine default +mode = "strict" # strict | casual + +[hotkey] +key = "cmd_r" # cmd_r | cmd_l | alt_r | alt_l | ctrl_r | ctrl_l + +[behavior] +pause_music = false +save_history = true ``` -Hold **Right Command** to record, release to transcribe and paste. +### Cleanup engine -### Strict vs Casual Mode +Cleanup runs **in-process via [mlx-lm](https://github.com/ml-explore/mlx-lm)** by default +(`Qwen2.5-1.5B-Instruct`, non-thinking) — no daemon, no HTTP, same MLX stack as transcription. -| Raw transcription | Strict | Casual | -|-------------------|--------|--------| -| "Hey um I'll see you tomorrow at 9 actually no make it 10" | "Hey, I'll see you tomorrow at 10." | "Hey, I'll see you tomorrow at 9, actually no, make it 10." | -| "So basically I was thinking we could um you know maybe try the other approach" | "I was thinking we could try the other approach." | "So basically, I was thinking we could maybe try the other approach." | +Already running **[Ollama](https://ollama.com)**? Switch to it (`v2t setup` offers this when it +detects Ollama, or edit the config): +```toml +[cleanup] +engine = "ollama" +model = "qwen3:4b-instruct-2507" # then: ollama pull qwen3:4b-instruct-2507 +``` -**Strict** (default): Removes filler words, restructures for clarity, condenses. +Either way, use a **non-thinking** model — a model that emits `` blocks will paste its +reasoning. The defaults don't. -**Casual**: Only adds punctuation and removes "um/uh", keeps your phrasing. +## Optional menu-bar app + +```bash +v2t menubar install # compile the bundled Swift file into ~/Applications/Voice2Text.app +``` -### `--pause-music` (macOS only) +The optional app is a single, inspectable Swift source file — no window, Xcode project, AppleScript, +or separate settings system. It exists because macOS only grants microphone access to a real app +identity. The menu requests the two native grants, starts one long-running Python process, shows +state immediately, and links to config, history, and log. Run `v2t` in a terminal instead if you do +not want the menu. -Pauses any playing media while recording and resumes after. Requires: +**Start v2t** loads Parakeet and the cleanup model once, then keeps them warm for every +transcription. To start the same menu app at login, install the optional per-user LaunchAgent: ```bash -brew install nowplaying-cli +v2t service install # install + start ~/Library/LaunchAgents/com.lucharo.voice2text.plist +v2t service status +v2t service uninstall ``` -Not available via pixi/conda-forge for now, maybe will publish later! +The service starts the same `Voice2Text.app` bundle, so manual and login launches share one stable +permission identity. The engine lock still prevents duplicate Python processes. + +**Permissions.** v2t needs **Microphone** (record) and **Accessibility** (global hotkey + paste). A +terminal launch uses your terminal's grants. The menu app requests its own grants and +shows their live state as flat rows; click a missing row to open the exact Settings pane. + +## Models & benchmarks + +The contributor-only `just bench` harness writes `~/.v2t/benchmarks/results/-.md` — two +tables (speech-to-text RTF and cleanup TTFT/total), one column per model. It lives in the dev group, +not the installed CLI. See [`benchmarks/`](benchmarks/) for the method and defaults. + +| | default | why | +|---|---|---| +| transcription | `parakeet-tdt-0.6b-v3` | fastest on Apple Silicon, multilingual | +| cleanup | `Qwen2.5-1.5B-Instruct` (mlx-lm) | 831 MB active in the local cleanup benchmark; non-thinking, no daemon | +> This is **macOS / Apple Silicon-only** by design (MLX, native pasteboard/event APIs, +> `nowplaying-cli`, System Settings permission URLs). Fork it for Linux/Windows if you like. diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..7c5d45d --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,61 @@ +# Benchmarks + +Two tables, models-as-columns, **median of N runs per cell**: + +- **Speech-to-text** — transcription time and real-time factor (RTF = audio length ÷ processing time; higher is faster). Inputs are generated on the fly with macOS `say`, so the benchmark is self-contained. +- **Text cleanup** — time-to-first-token and total time for MLX or Ollama cleanup models. + +Absolute numbers reflect the machine they ran on. **Compare models within a file, not across machines** — that's the point of one file per host. + +## Run it + +```bash +just bench # both tables, default models +just bench --cleanup # cleanup only (default models use MLX) +just bench --cleanup --cleanup-models ollama:qwen3:4b-instruct-2507 # Ollama/Linux +just bench --stt # STT only (needs Apple Silicon / MLX) +just bench --repeat 5 # more runs per cell +``` + +Override the model lists: + +```bash +just bench --stt-models \ + parakeet:mlx-community/parakeet-tdt-0.6b-v3 \ + parakeet:mlx-community/parakeet-tdt-0.6b-v2 \ + whisper:mlx-community/whisper-large-v3-turbo + +just bench --cleanup-models \ + mlx:mlx-community/Qwen2.5-1.5B-Instruct-4bit \ + mlx:mlx-community/Qwen3-4B-Instruct-2507-4bit \ + ollama:qwen3:4b-instruct-2507 +``` + +Cleanup models are `engine:model` specs (`mlx:…` or `ollama:…`); a model that isn't +installed is marked `n/a` rather than aborting the run. Each run writes +`~/.v2t/benchmarks/results/-.md`; copy results into this directory when contributing +to the grid. + +## Default models + +| | model | notes | +|---|---|---| +| STT (default) | `mlx-community/parakeet-tdt-0.6b-v3` | multilingual (EN/ES/FR/DE…), fastest on Apple Silicon | +| STT (alt) | `mlx-community/parakeet-tdt-0.6b-v2` | English-only, slightly higher EN accuracy | +| STT (fallback) | `mlx-community/whisper-large-v3-turbo` | the old default; best for rare languages/accents | +| cleanup (default) | `mlx:mlx-community/Qwen2.5-1.5B-Instruct-4bit` | in-process, non-thinking; 831 MB active in the local benchmark | +| cleanup (quality alt) | `mlx:mlx-community/Qwen3-4B-Instruct-2507-4bit` | 4B previous default; higher memory | +| cleanup (ollama) | `ollama:qwen3:4b-instruct-2507` | if you already run Ollama | + +> **Use a non-thinking model.** Hybrid Qwen3 tags emit `` blocks that +> add latency and pollute the output; the `-instruct-2507` and Qwen2.5-Instruct +> models above don't think. + +## Method + +- Models loaded once, then each sample transcribed/cleaned `--repeat` times; the median is reported. +- STT inputs: short / medium / long clips synthesized with `say` and converted to 16 kHz mono with `afconvert` — no audio files to ship, fully reproducible. +- Cleanup inputs: three filler-laden raw transcriptions (see `v2t/bench.py`). +- TTFT comes from streaming generation (mlx-lm or the Ollama HTTP API) and timing the first token. + +See `results/` for collected runs. diff --git a/benchmarks/results/.gitkeep b/benchmarks/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/results/2026-06-16-Apple-M1-Pro.md b/benchmarks/results/2026-06-16-Apple-M1-Pro.md new file mode 100644 index 0000000..42f579b --- /dev/null +++ b/benchmarks/results/2026-06-16-Apple-M1-Pro.md @@ -0,0 +1,25 @@ +# v2t benchmark — Apple-M1-Pro — 2026-06-16 + +_Compare models within this file; absolute times are machine-specific._ + +**Speech-to-text** — time & RTF + +| sample | Parakeet parakeet-tdt-0.6b-v3 | Parakeet parakeet-tdt-0.6b-v2 | Whisper whisper-large-v3-turbo | +|---|--:|--:|--:| +| load | 2.0s | 0.8s | 0.1s | +| short (3.4s) | 0.17s (21x) | 0.18s (19x) | 1.49s (2x) | +| medium (10.5s) | 0.29s (36x) | 0.27s (40x) | 1.61s (7x) | +| long (28.5s) | 0.64s (44x) | 0.58s (49x) | 1.92s (15x) | + +_Cells: transcription time (real-time factor). Higher RTF = faster._ + +**Text cleanup** — TTFT & total + +| sample | mlx:Qwen3-4B-Instruct-2507-4bit | mlx:Qwen2.5-3B-Instruct-4bit | +|---|--:|--:| +| 0 | 0.55 / 0.97s | 0.32 / 0.51s | +| 1 | 0.42 / 0.66s | 0.30 / 0.48s | +| 2 | 0.56 / 0.98s | 0.29 / 0.53s | +| **median** | 0.55 / 0.97s | 0.30 / 0.51s | + +_Cells: time-to-first-token / total. Lower is better._ diff --git a/benchmarks/results/GRID.md b/benchmarks/results/GRID.md new file mode 100644 index 0000000..b35289f --- /dev/null +++ b/benchmarks/results/GRID.md @@ -0,0 +1,66 @@ +# Cross-machine grid + +The rollup view: rows = machines, columns = models. Fill each cell by running +`just bench` on that machine (it also writes a detailed `-.md` here). +Compare a column down the rows to see how a model scales with hardware. + +## Speech-to-text — transcription time on the medium clip (RTF) + +_Lower time / higher RTF is better. Needs Apple Silicon (MLX)._ + +| machine | parakeet-tdt-0.6b-v3 | parakeet-tdt-0.6b-v2 | whisper-large-v3-turbo | +|---|--:|--:|--:| +| Linux x86 (remote container) | n/a — no MLX | n/a — no MLX | n/a — no MLX | +| MacBook (personal) — M1 Pro | 0.29s (36×) | 0.27s (40×) | 1.61s (7×) | +| MacBook (work) | _todo_ | _todo_ | _todo_ | + +## Text cleanup — TTFT / total on the median sample + +_Lower is better. mlx engine needs Apple Silicon; ollama engine needs Ollama._ + +| machine | mlx:Qwen3-4B-Instruct-2507-4bit | mlx:Qwen2.5-3B-Instruct-4bit | ollama:qwen3:4b-instruct-2507 | +|---|--:|--:|--:| +| Linux x86 (remote container) | n/a — no MLX | n/a — no MLX | _todo¹_ | +| MacBook (personal) — M1 Pro | 0.55 / 0.97s | 0.30 / 0.51s | _not run_ | +| MacBook (work) | _todo_ | _todo_ | _todo_ | + +¹ The default cleanup (mlx-lm) needs Apple Silicon. The remote Linux box can only +run the `ollama:` column, and only once Ollama is installed — CPU x86 timings +aren't representative of a Mac, so treat that cell as a sanity check, not a target. + +_Detailed per-run numbers: [`2026-06-16-Apple-M1-Pro.md`](2026-06-16-Apple-M1-Pro.md)._ + +## Transcript quality (accuracy) + +Speed ≠ accuracy. The benchmark clips are macOS `say` (synthetic, clean), so all +three STT models score ~0–2% WER on them locally — a sanity check (they transcribe +correctly and punctuate/capitalise well), too easy to separate models. + +For real-world accuracy, trust the [Open ASR Leaderboard](https://huggingface.co/spaces/hf-audio/open_asr_leaderboard), +which computes WER across 8 datasets with proper normalisation (more rigorous than +a quick local run): + +| model | English avg WER | notes | +|---|--:|---| +| parakeet-tdt-0.6b-v2 | ~6.0% | English-only; topped the leaderboard | +| **parakeet-tdt-0.6b-v3** (default) | ~6.3% | multilingual; small English cost vs v2 | +| whisper-large-v3-turbo | ~7.5–8% | distilled large-v3, trades accuracy for speed | + +So the default (**Parakeet v3**) is ~6.3% WER *and* the fastest here — the right +call. Switch to v2 (`model = "mlx-community/parakeet-tdt-0.6b-v2"`) for a hair more +English accuracy if you never need other languages. Cleanup can't recover missing +words, so STT WER is the accuracy that matters; punctuation/casing it fixes anyway. + +--- + +**How to fill this in** + +```bash +# on each Mac: +just bench # both tables -> ~/.v2t/benchmarks/results/-.md +# on a Linux box with Ollama: +just bench --cleanup --cleanup-models ollama:qwen3:4b-instruct-2507 +``` + +Then copy the medium-clip / median-sample numbers from each per-host file into +the rows above. diff --git a/justfile b/justfile index 3b95f98..1304fe4 100644 --- a/justfile +++ b/justfile @@ -1,15 +1,30 @@ # Lint with ruff lint: - uv run ruff check --fix voice2text.py + uv run ruff check v2t/ tests/ + +# Fast checks; no models, microphone, or permissions needed +check: lint + uv run python -m unittest -v tests.test_smoke + uv run python -m v2t.config + uv run python -m v2t.backends + xcrun swiftc -typecheck -parse-as-library v2t/native/Voice2Text.swift -framework AppKit -framework AVFoundation -framework ApplicationServices + +# Contributor benchmark harness (the dev group includes optional Whisper) +bench *args: + uv run python -m v2t.bench {{args}} # Build the package -build: +build: check rm -rf dist/ - uv run hatch build + uv build + uvx twine check dist/* # Publish to PyPI (reads ~/.pypirc) publish: uvx twine upload dist/* -# Build and publish -release: build publish +# Build and publish only from a clean checkout +release: + @test -z "$(git status --porcelain --untracked-files=all)" || (echo "working tree must be clean"; exit 1) + just build + just publish diff --git a/pixi.lock b/pixi.lock deleted file mode 100644 index 323b62a..0000000 --- a/pixi.lock +++ /dev/null @@ -1,1044 +0,0 @@ -version: 6 -environments: - default: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit - packages: - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h9a5124b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ollama-0.13.2-cpu_h19ea9b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/40/eb2f3a2c09bebf2fc989ba8bf701ce1f56b2f054b51e1a0fcb3e5d23f13a/huggingface_hub-1.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fe/a2/078152b45aa8a23949a1b09601d0044f8bb4ab85e909e4475a440c21aaea/mlx-0.29.3-cp313-cp313-macosx_13_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/41/95/a00054a006df82bb1b5b8f666ae44a676b259146fadbff90fe654309fefc/mlx_metal-0.29.3-py3-none-macosx_13_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/22/b7/a35232812a2ccfffcb7614ba96a91338551a660a0e9815cee668bf5743f0/mlx_whisper-0.4.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: ./ -packages: -- pypi: https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl - name: anyio - version: 4.12.0 - sha256: dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb - requires_dist: - - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - - idna>=2.8 - - typing-extensions>=4.5 ; python_full_version < '3.13' - - trio>=0.32.0 ; python_full_version >= '3.10' and extra == 'trio' - - trio>=0.31.0 ; python_full_version < '3.10' and extra == 'trio' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 - md5: 58fd217444c2a5701a44244faf518206 - depends: - - __osx >=11.0 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 125061 - timestamp: 1757437486465 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773 - md5: f0991f0f84902f6b6009b4d2350a83aa - depends: - - __unix - license: ISC - purls: [] - size: 152432 - timestamp: 1762967197890 -- pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - name: certifi - version: 2025.11.12 - sha256: 97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - name: cffi - version: 2.0.0 - sha256: 45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca - requires_dist: - - pycparser ; implementation_name != 'PyPy' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl - name: charset-normalizer - version: 3.4.4 - sha256: e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - name: click - version: 8.3.1 - sha256: 981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 - requires_dist: - - colorama ; sys_platform == 'win32' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - name: filelock - version: 3.20.0 - sha256: 339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl - name: fsspec - version: 2025.12.0 - sha256: 8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b - requires_dist: - - adlfs ; extra == 'abfs' - - adlfs ; extra == 'adl' - - pyarrow>=1 ; extra == 'arrow' - - dask ; extra == 'dask' - - distributed ; extra == 'dask' - - pre-commit ; extra == 'dev' - - ruff>=0.5 ; extra == 'dev' - - numpydoc ; extra == 'doc' - - sphinx ; extra == 'doc' - - sphinx-design ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - yarl ; extra == 'doc' - - dropbox ; extra == 'dropbox' - - dropboxdrivefs ; extra == 'dropbox' - - requests ; extra == 'dropbox' - - adlfs ; extra == 'full' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' - - dask ; extra == 'full' - - distributed ; extra == 'full' - - dropbox ; extra == 'full' - - dropboxdrivefs ; extra == 'full' - - fusepy ; extra == 'full' - - gcsfs ; extra == 'full' - - libarchive-c ; extra == 'full' - - ocifs ; extra == 'full' - - panel ; extra == 'full' - - paramiko ; extra == 'full' - - pyarrow>=1 ; extra == 'full' - - pygit2 ; extra == 'full' - - requests ; extra == 'full' - - s3fs ; extra == 'full' - - smbprotocol ; extra == 'full' - - tqdm ; extra == 'full' - - fusepy ; extra == 'fuse' - - gcsfs ; extra == 'gcs' - - pygit2 ; extra == 'git' - - requests ; extra == 'github' - - gcsfs ; extra == 'gs' - - panel ; extra == 'gui' - - pyarrow>=1 ; extra == 'hdfs' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' - - libarchive-c ; extra == 'libarchive' - - ocifs ; extra == 'oci' - - s3fs ; extra == 's3' - - paramiko ; extra == 'sftp' - - smbprotocol ; extra == 'smb' - - paramiko ; extra == 'ssh' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' - - numpy ; extra == 'test' - - pytest ; extra == 'test' - - pytest-asyncio!=0.22.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-recording ; extra == 'test' - - pytest-rerunfailures ; extra == 'test' - - requests ; extra == 'test' - - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' - - dask[dataframe,test] ; extra == 'test-downstream' - - moto[server]>4,<5 ; extra == 'test-downstream' - - pytest-timeout ; extra == 'test-downstream' - - xarray ; extra == 'test-downstream' - - adlfs ; extra == 'test-full' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' - - cloudpickle ; extra == 'test-full' - - dask ; extra == 'test-full' - - distributed ; extra == 'test-full' - - dropbox ; extra == 'test-full' - - dropboxdrivefs ; extra == 'test-full' - - fastparquet ; extra == 'test-full' - - fusepy ; extra == 'test-full' - - gcsfs ; extra == 'test-full' - - jinja2 ; extra == 'test-full' - - kerchunk ; extra == 'test-full' - - libarchive-c ; extra == 'test-full' - - lz4 ; extra == 'test-full' - - notebook ; extra == 'test-full' - - numpy ; extra == 'test-full' - - ocifs ; extra == 'test-full' - - pandas ; extra == 'test-full' - - panel ; extra == 'test-full' - - paramiko ; extra == 'test-full' - - pyarrow ; extra == 'test-full' - - pyarrow>=1 ; extra == 'test-full' - - pyftpdlib ; extra == 'test-full' - - pygit2 ; extra == 'test-full' - - pytest ; extra == 'test-full' - - pytest-asyncio!=0.22.0 ; extra == 'test-full' - - pytest-benchmark ; extra == 'test-full' - - pytest-cov ; extra == 'test-full' - - pytest-mock ; extra == 'test-full' - - pytest-recording ; extra == 'test-full' - - pytest-rerunfailures ; extra == 'test-full' - - python-snappy ; extra == 'test-full' - - requests ; extra == 'test-full' - - smbprotocol ; extra == 'test-full' - - tqdm ; extra == 'test-full' - - urllib3 ; extra == 'test-full' - - zarr ; extra == 'test-full' - - zstandard ; python_full_version < '3.14' and extra == 'test-full' - - tqdm ; extra == 'tqdm' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - name: h11 - version: 0.16.0 - sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl - name: hf-xet - version: 1.2.0 - sha256: 27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4 - requires_dist: - - pytest ; extra == 'tests' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - name: httpcore - version: 1.0.9 - sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 - requires_dist: - - certifi - - h11>=0.16 - - anyio>=4.0,<5.0 ; extra == 'asyncio' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - trio>=0.22.0,<1.0 ; extra == 'trio' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - name: httpx - version: 0.28.1 - sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - requires_dist: - - anyio - - certifi - - httpcore==1.* - - idna - - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' - - click==8.* ; extra == 'cli' - - pygments==2.* ; extra == 'cli' - - rich>=10,<14 ; extra == 'cli' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - zstandard>=0.18.0 ; extra == 'zstd' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/71/40/eb2f3a2c09bebf2fc989ba8bf701ce1f56b2f054b51e1a0fcb3e5d23f13a/huggingface_hub-1.2.2-py3-none-any.whl - name: huggingface-hub - version: 1.2.2 - sha256: 0f55d7d22058fbf8b29d8095aeee80a7b695aa764f906a21e886c1f87223718f - requires_dist: - - filelock - - fsspec>=2023.5.0 - - hf-xet>=1.2.0,<2.0.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' - - httpx>=0.23.0,<1 - - packaging>=20.9 - - pyyaml>=5.1 - - shellingham - - tqdm>=4.42.1 - - typer-slim - - typing-extensions>=3.7.4.3 - - authlib>=1.3.2 ; extra == 'oauth' - - fastapi ; extra == 'oauth' - - httpx ; extra == 'oauth' - - itsdangerous ; extra == 'oauth' - - torch ; extra == 'torch' - - safetensors[torch] ; extra == 'torch' - - toml ; extra == 'fastai' - - fastai>=2.4 ; extra == 'fastai' - - fastcore>=1.3.27 ; extra == 'fastai' - - hf-xet>=1.1.3,<2.0.0 ; extra == 'hf-xet' - - mcp>=1.8.0 ; extra == 'mcp' - - authlib>=1.3.2 ; extra == 'testing' - - fastapi ; extra == 'testing' - - httpx ; extra == 'testing' - - itsdangerous ; extra == 'testing' - - jedi ; extra == 'testing' - - jinja2 ; extra == 'testing' - - pytest>=8.4.2 ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-env ; extra == 'testing' - - pytest-xdist ; extra == 'testing' - - pytest-vcr ; extra == 'testing' - - pytest-asyncio ; extra == 'testing' - - pytest-rerunfailures<16.0 ; extra == 'testing' - - pytest-mock ; extra == 'testing' - - urllib3<2.0 ; extra == 'testing' - - soundfile ; extra == 'testing' - - pillow ; extra == 'testing' - - numpy ; extra == 'testing' - - fastapi ; extra == 'testing' - - typing-extensions>=4.8.0 ; extra == 'typing' - - types-pyyaml ; extra == 'typing' - - types-simplejson ; extra == 'typing' - - types-toml ; extra == 'typing' - - types-tqdm ; extra == 'typing' - - types-urllib3 ; extra == 'typing' - - ruff>=0.9.0 ; extra == 'quality' - - mypy==1.15.0 ; extra == 'quality' - - libcst>=1.4.0 ; extra == 'quality' - - ty ; extra == 'quality' - - authlib>=1.3.2 ; extra == 'all' - - fastapi ; extra == 'all' - - httpx ; extra == 'all' - - itsdangerous ; extra == 'all' - - jedi ; extra == 'all' - - jinja2 ; extra == 'all' - - pytest>=8.4.2 ; extra == 'all' - - pytest-cov ; extra == 'all' - - pytest-env ; extra == 'all' - - pytest-xdist ; extra == 'all' - - pytest-vcr ; extra == 'all' - - pytest-asyncio ; extra == 'all' - - pytest-rerunfailures<16.0 ; extra == 'all' - - pytest-mock ; extra == 'all' - - urllib3<2.0 ; extra == 'all' - - soundfile ; extra == 'all' - - pillow ; extra == 'all' - - numpy ; extra == 'all' - - fastapi ; extra == 'all' - - ruff>=0.9.0 ; extra == 'all' - - mypy==1.15.0 ; extra == 'all' - - libcst>=1.4.0 ; extra == 'all' - - ty ; extra == 'all' - - typing-extensions>=4.8.0 ; extra == 'all' - - types-pyyaml ; extra == 'all' - - types-simplejson ; extra == 'all' - - types-toml ; extra == 'all' - - types-tqdm ; extra == 'all' - - types-urllib3 ; extra == 'all' - - authlib>=1.3.2 ; extra == 'dev' - - fastapi ; extra == 'dev' - - httpx ; extra == 'dev' - - itsdangerous ; extra == 'dev' - - jedi ; extra == 'dev' - - jinja2 ; extra == 'dev' - - pytest>=8.4.2 ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-env ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - pytest-vcr ; extra == 'dev' - - pytest-asyncio ; extra == 'dev' - - pytest-rerunfailures<16.0 ; extra == 'dev' - - pytest-mock ; extra == 'dev' - - urllib3<2.0 ; extra == 'dev' - - soundfile ; extra == 'dev' - - pillow ; extra == 'dev' - - numpy ; extra == 'dev' - - fastapi ; extra == 'dev' - - ruff>=0.9.0 ; extra == 'dev' - - mypy==1.15.0 ; extra == 'dev' - - libcst>=1.4.0 ; extra == 'dev' - - ty ; extra == 'dev' - - typing-extensions>=4.8.0 ; extra == 'dev' - - types-pyyaml ; extra == 'dev' - - types-simplejson ; extra == 'dev' - - types-toml ; extra == 'dev' - - types-tqdm ; extra == 'dev' - - types-urllib3 ; extra == 'dev' - requires_python: '>=3.9.0' -- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - name: idna - version: '3.11' - sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea - requires_dist: - - ruff>=0.6.2 ; extra == 'all' - - mypy>=1.11.2 ; extra == 'all' - - pytest>=8.3.2 ; extra == 'all' - - flake8>=7.1.1 ; extra == 'all' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - name: jinja2 - version: 3.1.6 - sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - requires_dist: - - markupsafe>=2.0 - - babel>=2.7 ; extra == 'i18n' - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.7-hf598326_0.conda - sha256: 4bdbef0241b52e7a8552e8af7425f0b56d5621dd69df46c816546fefa17d77ab - md5: 0de94f39727c31c0447e408c5a210a56 - depends: - - __osx >=11.0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 568715 - timestamp: 1764676451068 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda - sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 - md5: b79875dbb5b1db9a4a22a4520f918e1a - depends: - - __osx >=11.0 - constrains: - - expat 2.7.3.* - license: MIT - license_family: MIT - purls: [] - size: 67800 - timestamp: 1763549994166 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f - md5: 411ff7cd5d1472bba0f55c0faf04453b - depends: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: [] - size: 40251 - timestamp: 1760295839166 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 - md5: d6df911d4564d77c4374b02552cb17d1 - depends: - - __osx >=11.0 - constrains: - - xz 5.8.1.* - license: 0BSD - purls: [] - size: 92286 - timestamp: 1749230283517 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - sha256: 0a1875fc1642324ebd6c4ac864604f3f18f57fbcf558a8264f6ced028a3c75b2 - md5: 85ccccb47823dd9f7a99d2c7f530342f - depends: - - __osx >=11.0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 71829 - timestamp: 1748393749336 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h9a5124b_0.conda - sha256: a46b167447e2a9e38586320c30b29e3b68b6f7e6b873c18d6b1aa2efd2626917 - md5: 67e50e5bd4e5e2310d66b88c4da50096 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: blessing - purls: [] - size: 906292 - timestamp: 1764359907797 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 - depends: - - __osx >=11.0 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 46438 - timestamp: 1727963202283 -- pypi: https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl - name: llvmlite - version: 0.46.0 - sha256: 30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl - name: loguru - version: 0.7.3 - sha256: 31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c - requires_dist: - - colorama>=0.3.4 ; sys_platform == 'win32' - - aiocontextvars>=0.2.0 ; python_full_version < '3.7' - - win32-setctime>=1.0.0 ; sys_platform == 'win32' - - pre-commit==4.0.1 ; python_full_version >= '3.9' and extra == 'dev' - - tox==3.27.1 ; python_full_version < '3.8' and extra == 'dev' - - tox==4.23.2 ; python_full_version >= '3.8' and extra == 'dev' - - pytest==6.1.2 ; python_full_version < '3.8' and extra == 'dev' - - pytest==8.3.2 ; python_full_version >= '3.8' and extra == 'dev' - - pytest-cov==2.12.1 ; python_full_version < '3.8' and extra == 'dev' - - pytest-cov==5.0.0 ; python_full_version == '3.8.*' and extra == 'dev' - - pytest-cov==6.0.0 ; python_full_version >= '3.9' and extra == 'dev' - - pytest-mypy-plugins==1.9.3 ; python_full_version >= '3.6' and python_full_version < '3.8' and extra == 'dev' - - pytest-mypy-plugins==3.1.0 ; python_full_version >= '3.8' and extra == 'dev' - - colorama==0.4.5 ; python_full_version < '3.8' and extra == 'dev' - - colorama==0.4.6 ; python_full_version >= '3.8' and extra == 'dev' - - freezegun==1.1.0 ; python_full_version < '3.8' and extra == 'dev' - - freezegun==1.5.0 ; python_full_version >= '3.8' and extra == 'dev' - - exceptiongroup==1.1.3 ; python_full_version >= '3.7' and python_full_version < '3.11' and extra == 'dev' - - mypy==0.910 ; python_full_version < '3.6' and extra == 'dev' - - mypy==0.971 ; python_full_version == '3.6.*' and extra == 'dev' - - mypy==1.4.1 ; python_full_version == '3.7.*' and extra == 'dev' - - mypy==1.13.0 ; python_full_version >= '3.8' and extra == 'dev' - - sphinx==8.1.3 ; python_full_version >= '3.11' and extra == 'dev' - - sphinx-rtd-theme==3.0.2 ; python_full_version >= '3.11' and extra == 'dev' - - myst-parser==4.0.0 ; python_full_version >= '3.11' and extra == 'dev' - - build==1.2.2 ; python_full_version >= '3.11' and extra == 'dev' - - twine==6.0.1 ; python_full_version >= '3.11' and extra == 'dev' - requires_python: '>=3.5,<4.0' -- pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - name: markupsafe - version: 3.0.3 - sha256: 116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fe/a2/078152b45aa8a23949a1b09601d0044f8bb4ab85e909e4475a440c21aaea/mlx-0.29.3-cp313-cp313-macosx_13_0_arm64.whl - name: mlx - version: 0.29.3 - sha256: d59eccf6a1e1e131becc5a3910504507862da3a4e9b7bd9e73a625515d767844 - requires_dist: - - mlx-metal==0.29.3 ; sys_platform == 'darwin' - - nanobind==2.4.0 ; extra == 'dev' - - numpy ; extra == 'dev' - - pre-commit ; extra == 'dev' - - setuptools>=80 ; extra == 'dev' - - torch ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - mlx-cuda==0.29.3 ; sys_platform == 'linux' and extra == 'cuda' - - mlx-cpu==0.29.3 ; sys_platform == 'linux' and extra == 'cpu' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/41/95/a00054a006df82bb1b5b8f666ae44a676b259146fadbff90fe654309fefc/mlx_metal-0.29.3-py3-none-macosx_13_0_arm64.whl - name: mlx-metal - version: 0.29.3 - sha256: 27b5a4d905202a71e84d9fd559ea0236813f6f960ef494e5cafe9c45df4c9d7c - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/22/b7/a35232812a2ccfffcb7614ba96a91338551a660a0e9815cee668bf5743f0/mlx_whisper-0.4.3-py3-none-any.whl - name: mlx-whisper - version: 0.4.3 - sha256: 6b82b6597a994643a3e5496c7bc229a672e5ca308458455bfe276e76ae024489 - requires_dist: - - mlx>=0.11 - - numba - - numpy - - torch - - tqdm - - more-itertools - - tiktoken - - huggingface-hub - - scipy - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - name: more-itertools - version: 10.8.0 - sha256: 52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - name: mpmath - version: 1.3.0 - sha256: a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - requires_dist: - - pytest>=4.6 ; extra == 'develop' - - pycodestyle ; extra == 'develop' - - pytest-cov ; extra == 'develop' - - codecov ; extra == 'develop' - - wheel ; extra == 'develop' - - sphinx ; extra == 'docs' - - gmpy2>=2.1.0a4 ; platform_python_implementation != 'PyPy' and extra == 'gmpy' - - pytest>=4.6 ; extra == 'tests' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 - md5: 068d497125e4bf8a66bf707254fff5ae - depends: - - __osx >=11.0 - license: X11 AND BSD-3-Clause - purls: [] - size: 797030 - timestamp: 1738196177597 -- pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl - name: networkx - version: 3.6.1 - sha256: d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 - requires_dist: - - asv ; extra == 'benchmarking' - - virtualenv ; extra == 'benchmarking' - - numpy>=1.25 ; extra == 'default' - - scipy>=1.11.2 ; extra == 'default' - - matplotlib>=3.8 ; extra == 'default' - - pandas>=2.0 ; extra == 'default' - - pre-commit>=4.1 ; extra == 'developer' - - mypy>=1.15 ; extra == 'developer' - - sphinx>=8.0 ; extra == 'doc' - - pydata-sphinx-theme>=0.16 ; extra == 'doc' - - sphinx-gallery>=0.18 ; extra == 'doc' - - numpydoc>=1.8.0 ; extra == 'doc' - - pillow>=10 ; extra == 'doc' - - texext>=0.6.7 ; extra == 'doc' - - myst-nb>=1.1 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - osmnx>=2.0.0 ; extra == 'example' - - momepy>=0.7.2 ; extra == 'example' - - contextily>=1.6 ; extra == 'example' - - seaborn>=0.13 ; extra == 'example' - - cairocffi>=1.7 ; extra == 'example' - - igraph>=0.11 ; extra == 'example' - - scikit-learn>=1.5 ; extra == 'example' - - iplotx>=0.9.0 ; extra == 'example' - - lxml>=4.6 ; extra == 'extra' - - pygraphviz>=1.14 ; extra == 'extra' - - pydot>=3.0.1 ; extra == 'extra' - - sympy>=1.10 ; extra == 'extra' - - build>=0.10 ; extra == 'release' - - twine>=4.0 ; extra == 'release' - - wheel>=0.40 ; extra == 'release' - - changelist==0.5 ; extra == 'release' - - pytest>=7.2 ; extra == 'test' - - pytest-cov>=4.0 ; extra == 'test' - - pytest-xdist>=3.0 ; extra == 'test' - - pytest-mpl ; extra == 'test-extras' - - pytest-randomly ; extra == 'test-extras' - requires_python: '>=3.11,!=3.14.1' -- pypi: https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl - name: numba - version: 0.63.1 - sha256: 0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc - requires_dist: - - llvmlite>=0.46.0.dev0,<0.47 - - numpy>=1.22,<2.4 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl - name: numpy - version: 2.3.5 - sha256: aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188 - requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ollama-0.13.2-cpu_h19ea9b3_0.conda - sha256: 2d71d0c04a1a6a856e14147373cccc99beaa4fcc7d18f2afb9548fe1103f772b - md5: 70dc3952b80d824be4f86057e37f660b - depends: - - __osx >=11.0 - - libcxx >=19 - license: MIT - purls: [] - size: 9599569 - timestamp: 1765226601139 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 - md5: b34dc4172653c13dcf453862f251af2b - depends: - - __osx >=11.0 - - ca-certificates - license: Apache-2.0 - license_family: Apache - purls: [] - size: 3108371 - timestamp: 1762839712322 -- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - name: packaging - version: '25.0' - sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - name: pycparser - version: '2.23' - sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl - name: pynput - version: 1.8.1 - sha256: 42dfcf27404459ca16ca889c8fb8ffe42a9fe54f722fd1a3e130728e59e768d2 - requires_dist: - - six - - evdev>=1.3 ; 'linux' in sys_platform - - python-xlib>=0.17 ; 'linux' in sys_platform - - enum34 ; python_full_version == '2.7.*' - - pyobjc-framework-applicationservices>=8.0 ; sys_platform == 'darwin' - - pyobjc-framework-quartz>=8.0 ; sys_platform == 'darwin' -- pypi: https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl - name: pyobjc-core - version: '12.1' - sha256: 01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl - name: pyobjc-framework-applicationservices - version: '12.1' - sha256: bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38 - requires_dist: - - pyobjc-core>=12.1 - - pyobjc-framework-cocoa>=12.1 - - pyobjc-framework-quartz>=12.1 - - pyobjc-framework-coretext>=12.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl - name: pyobjc-framework-cocoa - version: '12.1' - sha256: 5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118 - requires_dist: - - pyobjc-core>=12.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl - name: pyobjc-framework-coretext - version: '12.1' - sha256: 7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f - requires_dist: - - pyobjc-core>=12.1 - - pyobjc-framework-cocoa>=12.1 - - pyobjc-framework-quartz>=12.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl - name: pyobjc-framework-quartz - version: '12.1' - sha256: 629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860 - requires_dist: - - pyobjc-core>=12.1 - - pyobjc-framework-cocoa>=12.1 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda - build_number: 100 - sha256: c476f4e9b6d97c46b496b442878924868a54e5727251549ebfc82027aa52af68 - md5: 18a8c69608151098a8fb75eea64cc266 - depends: - - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.3,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 - - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - license: Python-2.0 - purls: [] - size: 12920650 - timestamp: 1765020887340 - python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - build_number: 8 - sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 - md5: 94305520c52a4aa3f6c2b1ff6008d9f8 - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 7002 - timestamp: 1752805902938 -- pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - name: pyyaml - version: 6.0.3 - sha256: 2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 - md5: 63ef3f6e6d6d5c589e64f11263dc5676 - depends: - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 252359 - timestamp: 1740379663071 -- pypi: https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl - name: regex - version: 2025.11.3 - sha256: 04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - name: requests - version: 2.32.5 - sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 - requires_dist: - - charset-normalizer>=2,<4 - - idna>=2.5,<4 - - urllib3>=1.21.1,<3 - - certifi>=2017.4.17 - - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl - name: scipy - version: 1.16.3 - sha256: 16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d - requires_dist: - - numpy>=1.25.2,<2.6 - - pytest>=8.0.0 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.0.292 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - name: setuptools - version: 80.9.0 - sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 - requires_dist: - - pytest>=6,!=8.1.* ; extra == 'test' - - virtualenv>=13.0.0 ; extra == 'test' - - wheel>=0.44.0 ; extra == 'test' - - pip>=19.1 ; extra == 'test' - - packaging>=24.2 ; extra == 'test' - - jaraco-envs>=2.2 ; extra == 'test' - - pytest-xdist>=3 ; extra == 'test' - - jaraco-path>=3.7.2 ; extra == 'test' - - build[virtualenv]>=1.0.3 ; extra == 'test' - - filelock>=3.4.0 ; extra == 'test' - - ini2toml[lite]>=0.14 ; extra == 'test' - - tomli-w>=1.0.0 ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' - - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' - - pytest-home>=0.5 ; extra == 'test' - - pytest-subprocess ; extra == 'test' - - pyproject-hooks!=1.1 ; extra == 'test' - - jaraco-test>=5.5 ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pygments-github-lexers==0.0.5 ; extra == 'doc' - - sphinx-favicon ; extra == 'doc' - - sphinx-inline-tabs ; extra == 'doc' - - sphinx-reredirects ; extra == 'doc' - - sphinxcontrib-towncrier ; extra == 'doc' - - sphinx-notfound-page>=1,<2 ; extra == 'doc' - - pyproject-hooks!=1.1 ; extra == 'doc' - - towncrier<24.7 ; extra == 'doc' - - packaging>=24.2 ; extra == 'core' - - more-itertools>=8.8 ; extra == 'core' - - jaraco-text>=3.7 ; extra == 'core' - - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' - - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' - - wheel>=0.43.0 ; extra == 'core' - - platformdirs>=4.2.2 ; extra == 'core' - - jaraco-functools>=4 ; extra == 'core' - - more-itertools ; extra == 'core' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=2.2 ; extra == 'enabler' - - pytest-mypy ; extra == 'type' - - mypy==1.14.* ; extra == 'type' - - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' - - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - name: shellingham - version: 1.5.4 - sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - name: six - version: 1.17.0 - sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- pypi: https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl - name: sounddevice - version: 0.5.3 - sha256: 278dc4451fff70934a176df048b77d80d7ce1623a6ec9db8b34b806f3112f9c2 - requires_dist: - - cffi>=1.0 - - numpy ; extra == 'numpy' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - name: sympy - version: 1.14.0 - sha256: e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - requires_dist: - - mpmath>=1.1.0,<1.4 - - pytest>=7.1.0 ; extra == 'dev' - - hypothesis>=6.70.0 ; extra == 'dev' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl - name: tiktoken - version: 0.12.0 - sha256: a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 - requires_dist: - - regex>=2022.1.18 - - requests>=2.26.0 - - blobfile>=2 ; extra == 'blobfile' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 - md5: a73d54a5abba6543cb2f0af1bfbd6851 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3125484 - timestamp: 1763055028377 -- pypi: https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl - name: torch - version: 2.9.1 - sha256: 2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587 - requires_dist: - - filelock - - typing-extensions>=4.10.0 - - setuptools ; python_full_version >= '3.12' - - sympy>=1.13.3 - - networkx>=2.5.1 - - jinja2 - - fsspec>=0.8.5 - - nvidia-cuda-nvrtc-cu12==12.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cuda-runtime-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cuda-cupti-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cudnn-cu12==9.10.2.21 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cublas-cu12==12.8.4.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cufft-cu12==11.3.3.83 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-curand-cu12==10.3.9.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cusolver-cu12==11.7.3.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cusparse-cu12==12.5.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cusparselt-cu12==0.7.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-nccl-cu12==2.27.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-nvshmem-cu12==3.3.20 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-nvtx-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-nvjitlink-cu12==12.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - nvidia-cufile-cu12==1.13.1.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - triton==3.5.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' - - optree>=0.13.0 ; extra == 'optree' - - opt-einsum>=3.3 ; extra == 'opt-einsum' - - pyyaml ; extra == 'pyyaml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - name: tqdm - version: 4.67.1 - sha256: 26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 - requires_dist: - - colorama ; sys_platform == 'win32' - - pytest>=6 ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-timeout ; extra == 'dev' - - pytest-asyncio>=0.24 ; extra == 'dev' - - nbval ; extra == 'dev' - - requests ; extra == 'discord' - - slack-sdk ; extra == 'slack' - - requests ; extra == 'telegram' - - ipywidgets>=6 ; extra == 'notebook' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl - name: typer-slim - version: 0.20.0 - sha256: f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d - requires_dist: - - click>=8.0.0 - - typing-extensions>=3.7.4.3 - - shellingham>=1.3.0 ; extra == 'standard' - - rich>=10.11.0 ; extra == 'standard' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - name: typing-extensions - version: 4.15.0 - sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a - license: LicenseRef-Public-Domain - purls: [] - size: 122968 - timestamp: 1742727099393 -- pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - name: urllib3 - version: 2.6.2 - sha256: ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd - requires_dist: - - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' - - h2>=4,<5 ; extra == 'h2' - - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' - requires_python: '>=3.9' -- pypi: ./ - name: voice2text - version: 0.1.0 - sha256: ada7327128a261dcd02f3415d522a97c9342312fe0cb93927b4a602bfed6c180 - requires_dist: - - mlx-whisper - - sounddevice - - numpy - - scipy - - pynput - - loguru - requires_python: '>=3.11' - editable: true diff --git a/pyproject.toml b/pyproject.toml index 3cd5a13..f5592ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,40 +1,49 @@ [project] name = "voice2text" -version = "0.2.0" -description = "Local voice-to-text with Whisper + LLM cleanup" +version = "0.3.0" +description = "Local, MLX-first voice-to-text with LLM cleanup" readme = "README.md" license = "GPL-2.0-only" requires-python = ">=3.11" dependencies = [ - "mlx-whisper", "sounddevice", - "numpy", + "numpy<2.4", "scipy", "pynput", + "pyobjc-framework-AVFoundation; sys_platform == 'darwin'", "loguru", + "parakeet-mlx", + "mlx-lm", ] +# Mac-first: Parakeet (STT) + mlx-lm (cleanup) ship in core, so a bare +# `uv tool install voice2text` just works on Apple Silicon. Whisper is an +# optional alternative STT backend. +[project.optional-dependencies] +whisper = ["mlx-whisper"] + [project.scripts] -v2t = "voice2text:main" +v2t = "v2t.cli:main" [dependency-groups] -dev = ["hatch", "ruff"] +dev = ["hatch", "mlx-whisper", "ruff"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" -[tool.pixi.workspace] -channels = ["conda-forge"] -platforms = ["osx-arm64"] - -[tool.pixi.dependencies] -python = ">=3.11,<3.14" -ollama = "*" -# nowplaying-cli not on conda-forge, install via: brew install nowplaying-cli +[tool.hatch.build.targets.wheel] +packages = ["v2t"] +exclude = ["v2t/bench.py"] -[tool.pixi.pypi-dependencies] -voice2text = { path = ".", editable = true } - -[tool.pixi.tasks] -v2t = "v2t" +[tool.hatch.build.targets.sdist] +include = [ + "/CHANGELOG.md", + "/LICENSE", + "/README.md", + "/benchmarks", + "/justfile", + "/pyproject.toml", + "/tests", + "/v2t", +] diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..0dd6bd8 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,860 @@ +"""Small behavior checks for v2t's user-facing mechanics.""" + +from __future__ import annotations + +import contextlib +import io +import os +import plistlib +import queue +import signal +import stat +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest import mock + +import numpy as np + +from v2t import app, backends, bench, cli, config, menubar, permissions, service + + +class V2TSmokeTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + self.env = mock.patch.dict( + os.environ, {"V2T_HOME": self.tempdir.name}, clear=False + ) + self.env.start() + self.addCleanup(self.env.stop) + + def test_audio_device_failure_returns_to_error_state(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + lock = config.acquire_instance_lock() + self.addCleanup(lock.close) + + with ( + mock.patch.object( + app.sd, "InputStream", side_effect=RuntimeError("device unavailable") + ), + mock.patch.object(app.subprocess, "run") as run, + ): + voice.start_recording() + + self.assertFalse(voice.recording) + self.assertIsNone(voice.stream) + self.assertEqual(config.read_status()["state"], "error") + self.assertNotIn( + ["nowplaying-cli", "pause"], [call.args[0] for call in run.call_args_list] + ) + + def test_permission_check_reports_missing_accessibility(self): + with ( + mock.patch.object( + app.permissions, + "statuses", + return_value={ + "microphone": "granted", + "accessibility": "missing", + }, + ), + mock.patch.object(app.subprocess, "run") as run, + self.assertRaises(SystemExit), + ): + app.check_and_request_permissions() + + run.assert_any_call( + [ + "open", + "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility", + ], + check=False, + ) + self.assertIn("Accessibility", config.read_last_error()) + + def test_permission_check_requests_microphone_before_startup(self): + with ( + mock.patch.object( + app.permissions, + "statuses", + return_value={ + "microphone": "not-requested", + "accessibility": "granted", + }, + ), + mock.patch.object( + app.permissions, "request_microphone", return_value=True + ) as request, + ): + app.check_and_request_permissions() + + request.assert_called_once_with() + + def test_permission_statuses_use_native_macos_checks(self): + application_services = types.SimpleNamespace( + AXIsProcessTrusted=lambda: True, + ) + avfoundation = types.SimpleNamespace( + AVCaptureDevice=types.SimpleNamespace( + authorizationStatusForMediaType_=lambda _media: 3 + ), + AVMediaTypeAudio="audio", + ) + with ( + mock.patch.object(permissions.sys, "platform", "darwin"), + mock.patch.dict( + sys.modules, + { + "ApplicationServices": application_services, + "AVFoundation": avfoundation, + }, + ), + ): + states = permissions.statuses() + + self.assertEqual( + states, + {"microphone": "granted", "accessibility": "granted"}, + ) + + def test_microphone_request_waits_for_native_result(self): + device = types.SimpleNamespace( + authorizationStatusForMediaType_=lambda _media: 0, + requestAccessForMediaType_completionHandler_=lambda _media, + callback: callback(True), + ) + avfoundation = types.SimpleNamespace( + AVCaptureDevice=device, + AVMediaTypeAudio="audio", + ) + with mock.patch.dict(sys.modules, {"AVFoundation": avfoundation}): + self.assertTrue(permissions.request_microphone(timeout=0.1)) + + def test_status_reports_the_running_overrides(self): + cfg = config.Config(cleanup_enabled=False, mode="casual") + voice = app.VoiceToText(cfg) + lock = config.acquire_instance_lock() + self.addCleanup(lock.close) + + voice._set_state("idle") + output = io.StringIO() + with ( + contextlib.redirect_stdout(output), + ): + cli.cmd_status([]) + + self.assertEqual( + output.getvalue(), + "idle\tparakeet-v3\toff\tcasual\t\n", + ) + + def test_recording_cannot_restart_before_processing_begins(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + voice.recording = True + voice.record_start = app.time.perf_counter() + voice.frames = [np.ones((2, 1), dtype=np.float32)] + voice.stream = mock.Mock() + voice.stream.stop.side_effect = lambda: self.assertTrue( + voice.finalizing_recording + ) + + with mock.patch.object(app.sd, "InputStream") as input_stream: + voice.stop_recording() + voice.start_recording() + + self.assertTrue(voice.processing) + self.assertEqual(voice.frames, []) + frames, duration = voice.jobs.get_nowait() + self.assertEqual(len(frames), 1) + self.assertGreaterEqual(duration, 0) + input_stream.assert_not_called() + + def test_queued_transcription_runs_on_the_processing_thread(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + voice.jobs.put(([np.ones((8, 1), dtype=np.float32)], 1.0)) + + with mock.patch.object(voice, "process_audio") as process: + self.assertTrue(voice.process_next()) + + process.assert_called_once() + + def test_empty_job_poll_keeps_the_hotkey_loop_alive(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + + self.assertTrue(voice.process_next(timeout=0)) + + def test_signal_waits_for_the_active_transcription_before_cleanup(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + voice.instance_lock = config.acquire_instance_lock() + voice.processing = True + voice._set_state("transcribing") + voice._start_shutdown_watcher() + + voice._handle_signal(signal.SIGTERM) + voice.shutdown_watcher.join(timeout=1) + + self.assertTrue(voice.stopping) + self.assertEqual(config.running_pid(), os.getpid()) + voice._set_state("cleaning") + self.assertEqual(config.read_status()["state"], "stopping") + with self.assertRaises(queue.Empty): + voice.jobs.get_nowait() + voice._handle_signal(signal.SIGTERM) + self.assertTrue(voice.stopping) + with self.assertRaises(SystemExit): + voice._handle_signal(signal.SIGINT) + + voice.shutdown() + self.assertIsNone(config.running_pid()) + + def test_signal_requests_prompt_shutdown_when_no_transcription_is_active(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + voice.instance_lock = config.acquire_instance_lock() + voice.startup_complete = True + + voice._handle_signal(signal.SIGTERM) + + self.assertTrue(voice.stopping) + voice.shutdown() + self.assertIsNone(config.running_pid()) + + def test_signal_interrupts_model_startup(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + + with self.assertRaises(SystemExit): + voice._handle_signal(signal.SIGTERM) + + def test_run_drains_an_accepted_job_before_shutdown(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + + class Listener: + def __init__(self, **_callbacks): + pass + + def __enter__(self): + return self + + def __exit__(self, *_): + pass + + def is_alive(self): + return True + + pynput = types.ModuleType("pynput") + pynput.keyboard = types.SimpleNamespace(Listener=Listener) + + def queue_then_stop(): + voice.processing = True + voice.stopping = True + voice.jobs.put(([np.ones((8, 1), dtype=np.float32)], 1.0)) + + def finish_job(*_): + voice.processing = False + + with ( + mock.patch.dict(sys.modules, {"pynput": pynput}), + mock.patch.object(app, "_resolve_hotkey", return_value=object()), + mock.patch.object(app.signal, "signal"), + mock.patch.object(voice, "warmup", side_effect=queue_then_stop), + mock.patch.object( + voice, "process_audio", side_effect=finish_job + ) as process, + ): + voice.run() + + process.assert_called_once() + + def test_shutdown_blocks_late_recording_and_closes_a_racing_stream(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + config.ensure_dirs() + voice.stopping = True + stream = voice.stream = mock.Mock() + + with mock.patch.object(app.sd, "InputStream") as input_stream: + voice.start_recording() + voice.shutdown() + + input_stream.assert_not_called() + stream.stop.assert_called_once() + stream.close.assert_called_once() + + def test_shutdown_wins_if_requested_while_the_stream_starts(self): + voice = app.VoiceToText( + config.Config(cleanup_enabled=False, pause_music=True) + ) + stream = mock.Mock() + stream.start.side_effect = lambda: setattr(voice, "stopping", True) + + with ( + mock.patch.object(app.sd, "InputStream", return_value=stream), + mock.patch.object(app.subprocess, "run") as run, + ): + voice.start_recording() + + self.assertFalse(voice.recording) + stream.stop.assert_called_once() + stream.close.assert_called_once() + self.assertNotIn( + ["nowplaying-cli", "pause"], [call.args[0] for call in run.call_args_list] + ) + + def test_transcription_pipeline_pastes_cleanup_and_deletes_audio(self): + voice = app.VoiceToText(config.Config(save_history=False)) + audio_path = None + + def transcribe(path): + nonlocal audio_path + audio_path = path + return "raw words" + + voice.stt = mock.Mock(transcribe=transcribe) + voice.cleaner = mock.Mock(model_id="cleaner") + voice.cleaner.cleanup.return_value = ("Clean words.", 0.1, 0.2) + voice.processing = True + lock = config.acquire_instance_lock() + self.addCleanup(lock.close) + + with mock.patch.object(voice, "paste_to_cursor") as paste: + voice.process_audio([np.ones((8, 1), dtype=np.float32)], 1.0) + + paste.assert_called_once_with("Clean words.") + self.assertIsNotNone(audio_path) + self.assertFalse(Path(audio_path).exists()) + self.assertEqual(config.read_status()["state"], "idle") + + def test_clipboard_is_restored_when_paste_fails(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + original = mock.Mock() + original.types.return_value = ["public.png", "public.utf8-plain-text"] + original.dataForType_.side_effect = lambda kind: { + "public.png": b"", + "public.utf8-plain-text": b"text", + }[kind] + pasteboard = mock.Mock() + pasteboard.pasteboardItems.return_value = [original] + pasteboard.setString_forType_.return_value = True + restored_item = mock.Mock() + item_class = mock.Mock() + item_class.alloc.return_value.init.return_value = restored_item + fake_appkit = types.SimpleNamespace( + NSPasteboard=mock.Mock( + generalPasteboard=mock.Mock(return_value=pasteboard) + ), + NSPasteboardItem=item_class, + NSPasteboardTypeString="public.utf8-plain-text", + ) + fake_quartz = types.SimpleNamespace( + CGEventCreateKeyboardEvent=lambda *_args: object(), + CGEventPost=lambda *_args: None, + CGEventSetFlags=mock.Mock(side_effect=RuntimeError("paste failed")), + kCGEventFlagMaskCommand=1, + kCGHIDEventTap=0, + ) + + with ( + mock.patch.dict( + sys.modules, {"AppKit": fake_appkit, "Quartz": fake_quartz} + ), + self.assertRaisesRegex(RuntimeError, "paste failed"), + ): + voice.paste_to_cursor("dictated") + + self.assertEqual( + [call.args for call in restored_item.setData_forType_.call_args_list], + [(b"", "public.png"), (b"text", "public.utf8-plain-text")], + ) + pasteboard.writeObjects_.assert_called_once_with([restored_item]) + + def test_clipboard_is_not_restored_over_a_new_user_copy(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + pasteboard = mock.Mock() + pasteboard.pasteboardItems.return_value = [] + pasteboard.setString_forType_.return_value = True + pasteboard.changeCount.side_effect = [9, 9, 10, 11] + fake_appkit = types.SimpleNamespace( + NSPasteboard=mock.Mock( + generalPasteboard=mock.Mock(return_value=pasteboard) + ), + NSPasteboardItem=mock.Mock(), + NSPasteboardTypeString="public.utf8-plain-text", + ) + fake_quartz = types.SimpleNamespace( + CGEventCreateKeyboardEvent=lambda *_args: object(), + CGEventPost=lambda *_args: None, + CGEventSetFlags=lambda *_args: None, + kCGEventFlagMaskCommand=1, + kCGHIDEventTap=0, + ) + + with ( + mock.patch.dict( + sys.modules, {"AppKit": fake_appkit, "Quartz": fake_quartz} + ), + mock.patch.object(app.time, "sleep"), + ): + voice.paste_to_cursor("dictated") + + pasteboard.clearContents.assert_called_once() + pasteboard.writeObjects_.assert_not_called() + + def test_paste_aborts_before_clear_if_clipboard_snapshot_keeps_changing(self): + voice = app.VoiceToText(config.Config(cleanup_enabled=False)) + pasteboard = mock.Mock() + pasteboard.pasteboardItems.return_value = [] + pasteboard.changeCount.side_effect = [1, 2, 3, 4] + fake_appkit = types.SimpleNamespace( + NSPasteboard=mock.Mock( + generalPasteboard=mock.Mock(return_value=pasteboard) + ), + NSPasteboardItem=mock.Mock(), + NSPasteboardTypeString="public.utf8-plain-text", + ) + fake_quartz = types.SimpleNamespace( + CGEventCreateKeyboardEvent=lambda *_args: object(), + CGEventPost=lambda *_args: None, + CGEventSetFlags=lambda *_args: None, + kCGEventFlagMaskCommand=1, + kCGHIDEventTap=0, + ) + + with ( + mock.patch.dict( + sys.modules, {"AppKit": fake_appkit, "Quartz": fake_quartz} + ), + self.assertRaisesRegex(RuntimeError, "clipboard changed"), + ): + voice.paste_to_cursor("dictated") + + pasteboard.clearContents.assert_not_called() + + def test_history_is_private_and_valid_jsonl(self): + config.append_history({"raw": "hello", "clean": "Hello."}) + + path = config.history_path() + mode = stat.S_IMODE(path.stat().st_mode) + + self.assertEqual(mode, 0o600) + self.assertEqual(path.read_text().count("\n"), 1) + self.assertEqual(stat.S_IMODE(path.parent.stat().st_mode), 0o700) + + def test_custom_config_keeps_existing_parent_permissions(self): + parent = Path(self.tempdir.name) / "shared" + parent.mkdir(mode=0o755) + path = parent / "v2t.toml" + + config.write_config("[cleanup]\nenabled = false\n", path) + + self.assertEqual(stat.S_IMODE(parent.stat().st_mode), 0o755) + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + + def test_second_instance_lock_is_rejected(self): + first = config.acquire_instance_lock() + self.addCleanup(first.close) + + with self.assertRaises(BlockingIOError): + config.acquire_instance_lock() + + def test_stale_status_cannot_reuse_an_unrelated_live_pid(self): + config.ensure_dirs() + config.write_status({"pid": os.getpid(), "state": "idle"}) + + self.assertIsNone(config.read_status()) + self.assertFalse((config.run_dir() / "status.json").exists()) + + def test_missing_optional_stt_is_rendered_as_not_available(self): + samples = [("short", Path("short.wav"), 1.0)] + with mock.patch.object(backends, "make_stt", side_effect=SystemExit("missing")): + results = bench.bench_stt(["whisper:model"], samples, repeat=1) + + table = bench.md_stt_table(results, samples) + + self.assertIsNone(results["whisper:model"]) + self.assertIn("| load | n/a |", table) + self.assertIn("| short (1.0s) | n/a |", table) + + def test_setup_recommends_the_real_parakeet_install(self): + output = io.StringIO() + with ( + mock.patch("builtins.input", side_effect=["", "n"]), + contextlib.redirect_stdout(output), + ): + cli.cmd_setup([]) + + self.assertIn("uv tool install voice2text", output.getvalue()) + self.assertNotIn("voice2text[parakeet]", output.getvalue()) + self.assertEqual(stat.S_IMODE(config.config_path().stat().st_mode), 0o600) + + def test_setup_quotes_the_whisper_extra_for_zsh(self): + output = io.StringIO() + with ( + mock.patch("builtins.input", side_effect=["2", "n"]), + contextlib.redirect_stdout(output), + ): + cli.cmd_setup([]) + + self.assertIn("uv tool install 'voice2text[whisper]'", output.getvalue()) + + def test_menubar_install_builds_a_grantable_native_app(self): + destination = Path(self.tempdir.name) / "Voice2Text.app" + + def compile_app(command, **_kwargs): + if command[0] == "xcrun": + Path(command[command.index("-o") + 1]).touch(mode=0o755) + return mock.Mock(returncode=0) + + with ( + mock.patch.object(menubar, "app_path", return_value=destination), + mock.patch.object(menubar.sys, "platform", "darwin"), + mock.patch.object(menubar, "signing_identity", return_value="-"), + mock.patch.object(menubar.subprocess, "run", side_effect=compile_app), + mock.patch.dict( + os.environ, {"V2T_HOME": self.tempdir.name}, clear=True + ), + ): + installed = menubar.install() + + info = plistlib.loads( + (installed / "Contents" / "Info.plist").read_bytes() + ) + self.assertEqual( + info, + { + "CFBundleDevelopmentRegion": "en", + "CFBundleExecutable": "Voice2Text", + "CFBundleIdentifier": "com.lucharo.voice2text", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "Voice2Text", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "0.3.0", + "CFBundleVersion": "1", + "LSMinimumSystemVersion": "13.0", + "LSUIElement": True, + "NSMicrophoneUsageDescription": "Voice2Text uses the microphone for fully local transcription.", + "NSPrincipalClass": "NSApplication", + "V2THome": self.tempdir.name, + "V2TPythonExecutable": sys.executable, + }, + ) + self.assertEqual( + stat.S_IMODE( + (installed / "Contents" / "MacOS" / "Voice2Text").stat().st_mode + ), + 0o755, + ) + + def test_menubar_prefers_a_stable_apple_development_signature(self): + output = """\ + 1) ABCDEF \"Apple Development: Developer (TEAMID)\" + 2) 123456 \"Apple Distribution: Developer (TEAMID)\" + 2 valid identities found +""" + with mock.patch.object( + menubar.subprocess, + "run", + return_value=mock.Mock(stdout=output), + ): + self.assertEqual( + menubar.signing_identity(), + "Apple Development: Developer (TEAMID)", + ) + + def test_launch_agent_keeps_one_warm_v2t_process(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + app_executable = Path(self.tempdir.name) / "Voice2Text.app/Contents/MacOS/Voice2Text" + app_executable.parent.mkdir(parents=True) + app_executable.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(menubar, "app_executable", return_value=app_executable), + mock.patch.object(service, "loaded", return_value=False), + mock.patch.object(service, "service_pid", return_value=None), + mock.patch.object(service, "_launchctl") as launchctl, + mock.patch.object(service.sys, "platform", "darwin"), + ): + service.install() + + data = plistlib.loads(plist.read_bytes()) + self.assertEqual( + data["ProgramArguments"], + [ + str(app_executable), + "--start", + ], + ) + self.assertTrue(data["RunAtLoad"]) + self.assertNotIn("KeepAlive", data) + launchctl.assert_called_once_with( + "bootstrap", f"gui/{os.getuid()}", str(plist) + ) + + def test_service_start_does_not_restart_a_healthy_process(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object( + config, "read_status", return_value={"pid": 84, "state": "idle"} + ), + mock.patch.object(service, "_is_child", return_value=True), + mock.patch.object(service, "_launchctl") as launchctl, + ): + service.start() + + launchctl.assert_not_called() + + def test_service_start_restarts_a_launchd_menu_with_no_engine(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object(service, "engine_ready", side_effect=[False, True]), + mock.patch.object(config, "running_pid", return_value=None), + mock.patch.object(service, "owned_engine_pid", return_value=None), + mock.patch.object(config, "clear_last_error"), + mock.patch.object(service, "stop") as stop, + mock.patch.object(service, "_launchctl") as launchctl, + ): + service.start() + + stop.assert_called_once() + launchctl.assert_called_once_with("kickstart", service.target()) + + def test_service_start_waits_for_a_prelock_child(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object(service, "engine_ready", side_effect=[False, True]), + mock.patch.object(config, "running_pid", return_value=None), + mock.patch.object(service, "owned_engine_pid", return_value=84), + mock.patch.object(service, "_launchctl") as launchctl, + ): + service.start() + + launchctl.assert_not_called() + + def test_service_start_waits_until_models_are_ready(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + statuses = [ + {"pid": 84, "state": "loading-stt"}, + {"pid": 84, "state": "idle"}, + ] + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object(config, "running_pid", return_value=84), + mock.patch.object(service, "_is_child", return_value=True), + mock.patch.object(config, "read_status", side_effect=statuses) as status, + ): + service.start() + + self.assertEqual(status.call_count, 2) + + def test_service_does_not_accept_an_external_ready_engine(self): + with ( + mock.patch.object( + config, "read_status", return_value={"pid": 84, "state": "idle"} + ), + mock.patch.object(service, "_is_child", return_value=False), + ): + self.assertFalse(service.engine_ready(42)) + + def test_service_start_rejects_an_external_engine(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object(service, "engine_ready", return_value=False), + mock.patch.object(config, "running_pid", return_value=84), + mock.patch.object(service, "_is_child", return_value=False), + ): + with self.assertRaisesRegex(SystemExit, "outside the login service"): + service.start() + + def test_service_status_reports_an_external_engine_honestly(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", return_value=42), + mock.patch.object(config, "running_pid", return_value=84), + mock.patch.object(service, "_is_child", return_value=False), + ): + self.assertEqual( + service.status(), + "menu running; v2t is running outside the login service", + ) + + def test_service_start_clears_a_stale_error_before_bootstrap(self): + plist = Path(self.tempdir.name) / "com.lucharo.voice2text.plist" + plist.touch() + with ( + mock.patch.object(service, "plist_path", return_value=plist), + mock.patch.object(service, "service_pid", side_effect=[None, 42, 42]), + mock.patch.object(service.menubar, "running", return_value=False), + mock.patch.object(config, "running_pid", return_value=None), + mock.patch.object(service, "loaded", return_value=True), + mock.patch.object(service, "engine_ready", side_effect=[False, True]), + mock.patch.object(config, "clear_last_error") as clear_error, + mock.patch.object(config, "read_last_error", return_value=""), + mock.patch.object(service, "_launchctl"), + mock.patch.object(service.time, "sleep"), + ): + service.start() + + clear_error.assert_called_once() + + def test_service_stop_waits_for_menu_and_engine_to_exit(self): + with ( + mock.patch.object(service, "service_pid", side_effect=[42, None]), + mock.patch.object(service, "owned_engine_pid", return_value=None), + mock.patch.object(service, "_launchctl") as launchctl, + ): + service.stop() + + launchctl.assert_called_once_with("kill", "SIGTERM", service.target()) + + def test_service_finds_its_child_before_engine_status_exists(self): + with ( + mock.patch.object(config, "running_pid", return_value=None), + mock.patch.object( + service.subprocess, + "run", + return_value=mock.Mock(stdout="84\n", returncode=0), + ) as run, + ): + self.assertEqual(service.owned_engine_pid(42), 84) + + run.assert_called_once_with( + ["pgrep", "-P", "42"], + capture_output=True, + text=True, + check=False, + ) + + def test_service_stop_waits_only_for_its_owned_engine(self): + with ( + mock.patch.object(service, "service_pid", side_effect=[42, None]), + mock.patch.object(service, "owned_engine_pid", return_value=84), + mock.patch.object(service, "_pid_alive", side_effect=[True, False]), + mock.patch.object(service.os, "kill") as kill, + mock.patch.object(service, "_launchctl") as launchctl, + mock.patch.object(service.time, "sleep"), + ): + service.stop() + + kill.assert_called_once_with(84, signal.SIGTERM) + launchctl.assert_called_once_with("kill", "SIGTERM", service.target()) + + def test_service_preserves_an_external_engine(self): + with ( + mock.patch.object(service, "service_pid", side_effect=[42, None]), + mock.patch.object(service, "owned_engine_pid", return_value=None), + mock.patch.object(service.os, "kill") as kill, + mock.patch.object(service, "_launchctl"), + ): + service.stop() + + kill.assert_not_called() + + def test_service_stop_does_not_force_an_engine_already_stopping(self): + with ( + mock.patch.object(service, "service_pid", side_effect=[42, None]), + mock.patch.object(service, "owned_engine_pid", return_value=84), + mock.patch.object( + config, "read_status", return_value={"pid": 84, "state": "stopping"} + ), + mock.patch.object(service, "_pid_alive", side_effect=[True, False]), + mock.patch.object(service.os, "kill") as kill, + mock.patch.object(service, "_launchctl"), + mock.patch.object(service.time, "sleep"), + ): + service.stop() + + kill.assert_not_called() + + def test_service_stop_tolerates_engine_exit_before_signal(self): + with ( + mock.patch.object(service, "service_pid", side_effect=[42, None]), + mock.patch.object(service, "owned_engine_pid", return_value=84), + mock.patch.object(config, "read_status", return_value=None), + mock.patch.object(service, "_pid_alive", return_value=False), + mock.patch.object( + service.os, "kill", side_effect=ProcessLookupError + ), + mock.patch.object(service, "_launchctl"), + ): + service.stop() + + def test_stop_reports_graceful_shutdown_honestly(self): + output = io.StringIO() + with ( + mock.patch.object(config, "running_pid", return_value=42), + mock.patch.object(cli.os, "kill") as kill, + contextlib.redirect_stdout(output), + ): + cli.cmd_stop([]) + + kill.assert_called_once_with(42, signal.SIGTERM) + self.assertEqual(output.getvalue(), "stopping v2t (pid 42)\n") + + def test_force_stop_uses_sigkill(self): + with ( + mock.patch.object(config, "running_pid", return_value=42), + mock.patch.object(cli.os, "kill") as kill, + contextlib.redirect_stdout(io.StringIO()), + ): + cli.cmd_stop(["--force"]) + + kill.assert_called_once_with(42, signal.SIGKILL) + + def test_cleanup_modes_are_mutually_exclusive(self): + with ( + contextlib.redirect_stderr(io.StringIO()), + self.assertRaises(SystemExit), + ): + cli.cmd_run(["--casual", "--strict"]) + + def test_cleanup_refuses_to_return_a_capped_partial_result(self): + cleaner = object.__new__(backends.MLXCleanup) + cleaner.model = object() + cleaner.tokenizer = mock.Mock() + cleaner.tokenizer.apply_chat_template.return_value = "prompt" + cleaner.tokenizer.encode.return_value = list(range(10)) + response = type("Response", (), {"text": "x"}) + cleaner._stream = lambda *_args, **kwargs: ( + response() for _ in range(kwargs["max_tokens"]) + ) + + with self.assertRaisesRegex(RuntimeError, "token limit"): + cleaner.cleanup("hello") + + def test_cleanup_benchmark_skips_a_missing_engine(self): + with ( + mock.patch.object( + backends, "make_cleanup", side_effect=SystemExit("missing dependency") + ), + contextlib.redirect_stdout(io.StringIO()), + ): + results = bench.bench_cleanup(["mlx:model"], ["sample"], 1, "") + + self.assertIsNone(results["mlx:model"]) + + def test_benchmark_repeat_must_be_positive(self): + with ( + contextlib.redirect_stderr(io.StringIO()), + self.assertRaises(SystemExit), + ): + bench.main(["--repeat", "0"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 2938aed..f68bd02 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,20 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version < '3.12'", ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "anyio" version = "4.12.0" @@ -19,6 +29,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + [[package]] name = "backports-tarfile" version = "1.2.0" @@ -264,14 +343,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] [[package]] @@ -331,6 +410,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, ] +[[package]] +name = "dacite" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420, upload-time = "2025-02-05T09:27:29.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -418,31 +515,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" }, + { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, ] [[package]] @@ -475,23 +575,23 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.2.2" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { name = "filelock" }, { name = "fsspec" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "shellingham" }, { name = "tqdm" }, - { name = "typer-slim" }, + { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/51/6db95c854e5eb3af8e0edfbfad7588983f63be39662054a49d5e116fb65d/huggingface_hub-1.2.2.tar.gz", hash = "sha256:b5b97bd37f4fe5b898a467373044649c94ee32006c032ce8fb835abe9d92ea28", size = 614598, upload-time = "2025-12-10T14:51:50.208Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/27/629cfe58c582f92ded066c4a07d1a057ff617118ab7973200f770bd853cb/huggingface_hub-1.19.0.tar.gz", hash = "sha256:fd771622182d40977272a923953ee3b1b13538f9f8a7f5d78398f10af0f1c0bd", size = 824721, upload-time = "2026-06-11T12:33:18.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/40/eb2f3a2c09bebf2fc989ba8bf701ce1f56b2f054b51e1a0fcb3e5d23f13a/huggingface_hub-1.2.2-py3-none-any.whl", hash = "sha256:0f55d7d22058fbf8b29d8095aeee80a7b695aa764f906a21e886c1f87223718f", size = 520964, upload-time = "2025-12-10T14:51:48.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a5/558da89f66464d8d0229ff497e8b8666977de2d8cf48c28a2862ecf1250f/huggingface_hub-1.19.0-py3-none-any.whl", hash = "sha256:1dc72e1f6b4d6df6b30eb72e57d00514ef453d660f04af2b87f0e67267f31ee0", size = 693398, upload-time = "2026-06-11T12:33:16.695Z" }, ] [[package]] @@ -520,7 +620,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.12'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -584,6 +684,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + [[package]] name = "keyring" version = "25.7.0" @@ -602,6 +711,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pooch" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + [[package]] name = "llvmlite" version = "0.46.0" @@ -736,42 +883,60 @@ wheels = [ [[package]] name = "mlx" -version = "0.30.0" +version = "0.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/e8/69ebac29536c026489ded1ad58a6f5163b8fc10ab5eac21228f57ea9e83f/mlx-0.30.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:d5871a5f1ce5cba2f690d2c630a6672cc65a62326bcaa6db258185957a1f073f", size = 554825, upload-time = "2025-11-20T16:45:11.135Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c7/db80b1e9f613baf99745e9920a3a7fe7b6c61398420ed308f24f60877a15/mlx-0.30.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:302c3d52b4f68b80a8ee259489369524a3381fcc9aed12b17c1c537870c81fce", size = 554821, upload-time = "2025-11-20T01:16:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/8a/68/55cd5cab5a9b8b958ea995295aa3428ad65b3e893f04c27e2cdfcdc176af/mlx-0.30.0-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:d72443a461f2e92329edf344ded8460df53fd7ec3c04ddd2353ee4ceb34c0ff2", size = 554791, upload-time = "2025-11-20T05:33:23.851Z" }, - { url = "https://files.pythonhosted.org/packages/16/91/5b79b6febcbb7e1051ecf408a8c30226ebfdf19a6e304b4cfa32309059ab/mlx-0.30.0-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:9de2ab05cc9721c99a0802721aa1f0d6c305e97acd2f8ec4c05af3449ee7700d", size = 625176, upload-time = "2025-12-01T15:23:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/80/17/16868ac1ea36ea3baf61f84721b4dfd98dd247a230b951ddd715981db79b/mlx-0.30.0-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:0beb015a6afef2f86dfbefe65c9022dce91dbe1651e1b4330ef32434602323e0", size = 659735, upload-time = "2025-11-20T01:16:40.594Z" }, - { url = "https://files.pythonhosted.org/packages/94/a3/32c4c05d8967591e2a1a1e7e3fc9cece8821f5aea8ac8f3bcfdb203f4722/mlx-0.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:11fbae58b1e992afdec6709d5e281932871a0138582a794cdcc82ff895a28670", size = 554567, upload-time = "2025-11-20T16:45:12.73Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/b6143f1c078fbc873e40e624dc428a3ada240721001414955f584afa866d/mlx-0.30.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:fc1994a4c9e60ccfe2ab9211c846be2ef7abc6fc3e53e1addb2cdf7468f61e7b", size = 554566, upload-time = "2025-11-20T01:16:41.877Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/aec36297ef76b4b190cc6d4cea1ed995b458bbc21cb91c58b0862f8cae7d/mlx-0.30.0-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a9f30c5c94b30b65f026162e91256473187e1de57c13dcc2aedb2fc33c07f2c1", size = 554593, upload-time = "2025-11-20T05:33:25.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/d3/575eee4ef4b5f3dad9076a78f287affe046fd32b3bdba7a2e0af31f0d9d3/mlx-0.30.0-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f88d11e2719fdab08dbe68c9666003c8644ccd01dff973bc86e949cc16195eac", size = 612086, upload-time = "2025-12-01T15:23:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/23/37/b5dd68da0e79e258f5d6a0e9f5fd4f9e5452e92c20b13b64948a965ea429/mlx-0.30.0-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:1c4a2f5285bdd585aa6485a4fb5759ebc2721ba9381404ff867c136e84764e9b", size = 653994, upload-time = "2025-11-20T01:16:43.492Z" }, - { url = "https://files.pythonhosted.org/packages/33/a5/e171b2caa69b346bc1abc1bfd0b139f631f68a0ff602862dd255e7dd95ec/mlx-0.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f46aaa6c562ba183e2a64a0e6ba15ed54f9027d9b7b1822e9eec7f59b13d610c", size = 554595, upload-time = "2025-11-20T16:45:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f4/aeb8980bbef08fc031ab1a2d043a1d76e60d49bf46728bef66cf25b26dfa/mlx-0.30.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:5edee9f1452703c0804e067f212c61d488b3ad7419b9fcacf337ffd35842f576", size = 554593, upload-time = "2025-11-20T01:16:45.033Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/904235e11610c7cf2ba39eb39f32587d34048f7293d386f19d42d1e3dabc/mlx-0.30.0-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:b8ae92b61353d756bbd0668b065a4ee5ee35867b03d63c0c61134d656ed236fe", size = 554330, upload-time = "2025-11-20T05:33:26.653Z" }, - { url = "https://files.pythonhosted.org/packages/de/0f/bb956ec9926596fe771ec67677c049e358c43f0506699b59dbed7ba9fedc/mlx-0.30.0-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f3ae4c99308ff4c006c3062ff73108b6f39414bcd90416930b917160070f759b", size = 612083, upload-time = "2025-12-01T15:23:43.369Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/3f3505d3fbf0f977b2930b3596f590f0079c3a2a253d01349f936c40985a/mlx-0.30.0-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:431009c531f8bdbf56f46b85d458d5526b40ea43178dbc85f37ed55e03e716be", size = 653971, upload-time = "2025-11-20T01:16:46.292Z" }, - { url = "https://files.pythonhosted.org/packages/43/25/6fa174632f5beb583eda80902af80dc39a63e5b4b6e66c7831301751d82e/mlx-0.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:384819dccfd551aa1444acacedb9ed2619724b0e67fc361ab80d73b8a8a7618f", size = 558834, upload-time = "2025-11-20T16:45:15.303Z" }, - { url = "https://files.pythonhosted.org/packages/b9/00/8c93f6ba5dc37a459b378bd22d2303824aa341595ed6c4958c6a48870677/mlx-0.30.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8982a7d7fd078988c12c3fc53da1670aa910dd8a7df1fc0b9fee7303394ac8ff", size = 558833, upload-time = "2025-11-20T01:16:48.748Z" }, - { url = "https://files.pythonhosted.org/packages/58/65/0ecb3a858142d7b250b46d1e5c2bb4ebff9f57b735643626b7d4653b0f11/mlx-0.30.0-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:392af721cb0bcf600162d7258923c15d39e197a64e23b6abd171e1aea79771c5", size = 558510, upload-time = "2025-11-20T05:33:28.286Z" }, - { url = "https://files.pythonhosted.org/packages/27/a2/df61dc58caf6238d90cfa2535018fa2c52745e76421dd3ff11c034aae26f/mlx-0.30.0-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:a4dae4c0404560d3df9832ee709e502022a9e5f89c4d432c576af8405d9a45ef", size = 614193, upload-time = "2025-12-01T15:23:44.688Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dd/38f465477f996bb24ab133aaf79a3eb0fa13a9bd9d19504aad30c3be7ffc/mlx-0.30.0-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:cf688c9dda18f2521d48f712515a47b145b37d5d0ab245eaf1a68f286af7ae66", size = 654400, upload-time = "2025-11-20T01:16:50.018Z" }, + { url = "https://files.pythonhosted.org/packages/94/89/1e77ec3ff380e8fb9e7258047374d31452a0f9828a0e370f127b07dd8288/mlx-0.31.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4a3f181b367d404e44a6bd68ef5eb573930809ac60cacd51d0c851c629b1b651", size = 586911, upload-time = "2026-04-22T03:14:29.675Z" }, + { url = "https://files.pythonhosted.org/packages/6a/41/c1907f05f8a3fc54025fb78ad68d3c4a4b931664d03c0a24f7f431cc4087/mlx-0.31.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:70297cbef7479429f69c966bfed10da20a6f0c2aa997eec2b4f6ba1a07caf2ef", size = 586915, upload-time = "2026-04-22T03:14:31.403Z" }, + { url = "https://files.pythonhosted.org/packages/97/b0/61ac2c14773c786fecbda28067b0207a0c654cb4d10c548808c51284d700/mlx-0.31.2-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:c0ff158b7ac93a4b5659adbc70053498b30a5964fc45f78596398e056a96c36a", size = 587030, upload-time = "2026-04-22T03:14:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/de/53/e12feb7078ee472983555fcb1da4749a2bbbc8fc5b29b78c205b96d37d1e/mlx-0.31.2-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:cd5d42b0b2bee7efe1b0680a7e302943dd33b92c879cffa0358ffdb5a4a8d27b", size = 652994, upload-time = "2026-04-22T03:14:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/c5/40/f92c8cdc9595bf24c7e483a3156bfe0cc99a5cf5545d8dba8e7fe000c10b/mlx-0.31.2-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:b368f7ede4238cc44076e4843820338c453c21ee50bd3ee26d4b182c179fd8e1", size = 692086, upload-time = "2026-04-22T03:14:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/5f33906cb03d6a378a697cd2d2641a26b37dea17ee3d9124d7e39e8eca01/mlx-0.31.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e5067aaf2be1f3d7bba5be52348775804f111173c1ed04639618fd713b1a530f", size = 584863, upload-time = "2026-04-22T03:14:38.211Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/a851a451b1327af9fb4df3991b9ae87d066b6f6630e854af55c288b0995a/mlx-0.31.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:edb9797db7d852477ca1c99708058654ee860d4148fe5765f0d55528e2b1aa22", size = 584860, upload-time = "2026-04-22T03:14:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/0d1dc0597644e5e7b011ca954ba0c47e13cd880a3b909b0c3f1b4d8bf8f1/mlx-0.31.2-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:51ca102db641b01e7cb083ce8ecb580e281530a141a7ca12544bb370641630ae", size = 584887, upload-time = "2026-04-22T03:14:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c3/00664239a98e8bd614733c4182cd402d2bacad2d7f79eca66562ac406870/mlx-0.31.2-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:117c7583cae0ca107cd53c591cc34f8e75f97a505aa47088844b7dc0fc69dc67", size = 627863, upload-time = "2026-04-22T03:14:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/53/7b/af6cd73a79772af6f19eab2cb4c48eda23a9294d1650a4c1269a9996e532/mlx-0.31.2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:99572133181481640a8bf8d449daf083816d0af3ee050c8adfc5bf45ceca91c6", size = 685090, upload-time = "2026-04-22T03:14:45.058Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3f/888f8664d4f8e23a1363a5f50024be5216e199ab7ad0ba20988c7ed6d729/mlx-0.31.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1b3fb0dda955b0d552ce57bdd6f42b3309ab21b067e40587d6848443d307e91f", size = 584796, upload-time = "2026-04-22T03:14:47.215Z" }, + { url = "https://files.pythonhosted.org/packages/dd/14/e9cd18b51f9e1dbcb060eec0fafc2d2428c8e1eacd9b0a02d7c5ce75b661/mlx-0.31.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:34b0171cd9eb5c43fdd82091f6135d6ccc5a065363a4a3e68fac64fb4e53d37c", size = 584790, upload-time = "2026-04-22T03:14:48.519Z" }, + { url = "https://files.pythonhosted.org/packages/ca/20/c6c5fb998c7834d094b2bfb9f003b5246cb270f0266da055c55546c34999/mlx-0.31.2-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:c05981684279a8935d58b0dde3ea5b02d210c3bad3319aa0e9934ec2df165752", size = 584795, upload-time = "2026-04-22T03:14:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/0b/19/aca251d4c5f3532ce9c2c1e95ad76740d9c6c298f406f62d992f465b9be0/mlx-0.31.2-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:cd1f4189e5f1bc68735f44eb63ce98ae09d66ac75d7ab5b15a41afae7e9f0513", size = 627843, upload-time = "2026-04-22T03:14:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/b89364883b98f21c2fe29e52d4ac8bc2fa2fe0d79293b36ec421efc1854a/mlx-0.31.2-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:53c8d57ffa9ce77f8355663be05014c0dd37280e57f19126fb0a24389a30684b", size = 685064, upload-time = "2026-04-22T03:14:52.75Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/e63f6a9316ded2d14a8ebc7a9ca25734c784e8c54d064a78b4dceeacec0e/mlx-0.31.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a13c9ce23c3deef6aa5a09315e7953e1a5dc311e851fa16fc74c81fb2509c0b9", size = 588417, upload-time = "2026-04-22T03:14:54.094Z" }, + { url = "https://files.pythonhosted.org/packages/31/50/9d0c03ea3134cd85c132df7b0e4b75e6344bd8b4881a0b9c465cfa27f724/mlx-0.31.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b0764bf11fc3a71dee988e19275eef67775cab63112d8bb7ef173ca8b2a1247c", size = 588421, upload-time = "2026-04-22T03:14:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/d364cc793bcb504621313acb55627cf0d5403ab2e0a594aa081cdbe4591f/mlx-0.31.2-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:59ccbd0f0044d4f97f11ebcbf0c480bc9e962935fd96275f120954afea65be8a", size = 588384, upload-time = "2026-04-22T03:14:57.439Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a4/e822202dd2e4e7d08671f2ecf7b6500af74f5bad5ceb27086b1aa6902f3a/mlx-0.31.2-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:e81798c610f95a09c642c89214ba5c23b72ce18ce4728184aceabe7eddca33d7", size = 630473, upload-time = "2026-04-22T03:14:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/da48d2d7a76e644d35438ef6f33c68755fdd382e2c546fd1804ccba01d04/mlx-0.31.2-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:69fbc94bf53607a75af9eb3e22c354738a6fe4e25aa4e2b20934b009a4bba1f3", size = 685459, upload-time = "2026-04-22T03:15:00.45Z" }, +] + +[[package]] +name = "mlx-lm" +version = "0.31.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mlx", marker = "sys_platform == 'darwin'" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "sentencepiece" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/94/9a38d6b0c6fcca995b9136c94eb7da1e9c5165652edf228b96b29960fa7a/mlx_lm-0.31.3.tar.gz", hash = "sha256:61eb0e3ba09444f77f874aff295401d7ccd20b39495cbbce0c782a15474ce733", size = 304318, upload-time = "2026-04-22T07:37:27.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/02/9a67b8e4f87e3e2e5cd7b1ad79304b93c09a0db6af34bee75e6551c06c60/mlx_lm-0.31.3-py3-none-any.whl", hash = "sha256:758cfddf1180053b7613db76fad3d246a331a2a905808e1164a275621fc983b8", size = 408890, upload-time = "2026-04-22T07:37:25.965Z" }, ] [[package]] name = "mlx-metal" -version = "0.30.0" +version = "0.31.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/9f/47ebb6e9b2c33371c6ca3733e70324ed064f49e790ee4e194b713d6d7d84/mlx_metal-0.30.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:f48543b10d13bf0591b3f99c6eb585dd2c2e5db379edae5df0f19a728cb41742", size = 36819863, upload-time = "2025-11-20T06:42:23.927Z" }, - { url = "https://files.pythonhosted.org/packages/7f/91/c04e420326390c37d4c13f58960956d698cd34c7432ae3860bc5c6be71a0/mlx_metal-0.30.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:74bb5d10e0f24e21973d39430557bbd5d733c2a6599c3f1b87f9a0ff73fed2c8", size = 36817633, upload-time = "2025-11-20T01:16:27.77Z" }, - { url = "https://files.pythonhosted.org/packages/86/18/7af11eac0f488b68c436d249ffcf4f76003326e691994db3f720d21f21bb/mlx_metal-0.30.0-py3-none-macosx_26_0_arm64.whl", hash = "sha256:310fda8b2f7345865f3dd75a9b478e974f28d22e8ebf05f26af5adc0e8979cee", size = 44878409, upload-time = "2025-11-20T05:33:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/3f/69/fe3b783ebe999f3118234e1e940feb622518bfb1dea6ac5d13b1d36a8449/mlx_metal-0.31.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b25385bcee18fc194092255b8b53b9a3d8489eb650e59160f1b57aadd07aa2dc", size = 40055588, upload-time = "2026-04-22T03:14:14.43Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5d/4c690d5b93c30ba002656c37363159d978705bf8eb801b8481840fb942c2/mlx_metal-0.31.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:e9d4e5fce6ca10a87a0e388597f99519ad594d09e674708b5312bd8bd4f5997d", size = 40053220, upload-time = "2026-04-22T03:14:18.048Z" }, + { url = "https://files.pythonhosted.org/packages/99/82/11fd62a8d7a3e96e5c43220b17de0151e3f10101f8bb3b865f5bd9cdd074/mlx_metal-0.31.2-py3-none-macosx_26_0_arm64.whl", hash = "sha256:84ffb60ee503f03eb684f5fb168d5cff31e2a16b7f27c1731eaf7662bd6e9b46", size = 55792151, upload-time = "2026-04-22T03:14:22.059Z" }, ] [[package]] @@ -811,6 +976,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msgpack" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/23/6139781ca7aadf656fa8e384fa84693ffb13f299e6931b6526427fe5e297/msgpack-1.2.0.tar.gz", hash = "sha256:8e17af38197bf58e7e819041678f6178f4491493f5b8c8580414f40f7c2c3c41", size = 183017, upload-time = "2026-06-11T04:16:10.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/23/35de3182a647fcc84ab304160169edfa5dac7bbd8913fbed0a505ddc0d55/msgpack-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec35cd3f127f50806aa10c3f74bf27b749f13ddf1d2217964ada8f38042d1653", size = 82368, upload-time = "2026-06-11T04:14:53.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/79/8d9bfdab933b1c7a02aba9518605a81aa30d38e9efd4915ec1a6b2d55778/msgpack-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:317eb298297121bfad9173d748124a04a36af27b6ac39c2bbc1db1ce57608dcf", size = 82095, upload-time = "2026-06-11T04:14:54.784Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e1/b5accbc1354edbcee107fb35ec247db0547e91c3f90e4fabdeaee500a5a6/msgpack-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50fe6434de89073273026dd032a62e8b63f8857a261d7a2df5b07c9e72f3a8f7", size = 413818, upload-time = "2026-06-11T04:14:56.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/1141cbbf7118d525834f20dcd614d1b85f1f2ffd33bc2a5ce710e6dd2516/msgpack-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106c6d333ff3d4eda075b7d4b9695d1752c5bcc635e40d0dbaf4e276c9ed80e1", size = 423790, upload-time = "2026-06-11T04:14:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/9582f2bd4d7546139fe297740de49bd1f7ef2d195eb0bb9fa5efeee88158/msgpack-1.2.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:67055a611e871cb1bd0acb732f2e9f64ca8155ca0bba1d0a5bb362e7209e5541", size = 387521, upload-time = "2026-06-11T04:14:59.08Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/5aadd08ff068bfd42e2ac0be6a20aa9819965df8622e87c1f0c6119c1c22/msgpack-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceec7f8e633d5a4b4a32b0416bef90ee3cd1017ea36247f705e523072e576119", size = 406324, upload-time = "2026-06-11T04:15:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/ee/3041564f0cc4c2fe7c53315aec0edf3d84807fc9b9ea714e6ac07dbdb1db/msgpack-1.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7ec5851160a3c2c0f77d68ddec620318cd8e7d88d94f9c058190e8ce0dfa1d31", size = 384242, upload-time = "2026-06-11T04:15:02.121Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/de94b3dbc266229f4c2ce84485eeb221220351b7f1931029e875995bb232/msgpack-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd7140f7b09dbe1984a0dff3189375d840247e3e4cf4ac45c5a499b3b599c8d2", size = 420392, upload-time = "2026-06-11T04:15:03.692Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5d/c4a3fde69a292eecb202caaa87c29df7728644a65118614b821bcaddc05a/msgpack-1.2.0-cp311-cp311-win32.whl", hash = "sha256:cbfd54018d386da0951c7a2be13de0f58559d251313e613b2155e52ed1cbd8f1", size = 63976, upload-time = "2026-06-11T04:15:05.355Z" }, + { url = "https://files.pythonhosted.org/packages/18/fa/df47f83115375e7717c985265a30f3ba096c5331518e28fb647b55c46d31/msgpack-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:653373c4614c31463ba486a67776e4bb396af289921bd5353e209534b71467fa", size = 70273, upload-time = "2026-06-11T04:15:06.529Z" }, + { url = "https://files.pythonhosted.org/packages/54/d1/ffd02e54c064aa73b6b53aa08171f92dc406727077ff275d7050c6aca28a/msgpack-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:7a260aea1e5e7d6c7f1d9284c7360d29021627b61dc4dd7df144b81210810537", size = 64783, upload-time = "2026-06-11T04:15:07.677Z" }, + { url = "https://files.pythonhosted.org/packages/44/07/dcb13f37e670257c8d0e944f116c799c34ac6968ecb48c83619f7e91d8b5/msgpack-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2d6047ccd11a12c96a69f2bfe026471abef67334c3d0494a93e5310e45140a2", size = 82888, upload-time = "2026-06-11T04:15:08.992Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/6643b2a6a36ca4bc73c7674831be1d4d581cceecc7eb019dba1915951739/msgpack-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0347e3ac0dfee99086d3b68fe959da3f5f657c0019ddbaeaaa259a85f8603422", size = 82223, upload-time = "2026-06-11T04:15:10.182Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c8/9e1668b9897358e5ab39a18142e38be3cf15807e643757782da9f4a53cb3/msgpack-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25552ff1f2ff3dc8333e27eabb94f702da5929ed0e07969688194a3e9f12e151", size = 409700, upload-time = "2026-06-11T04:15:11.441Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/b7728573156d70b6b094233b0f38d876fc37340826cf852347ec2c7ca8ca/msgpack-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0d94420d9d52c56568159a69200af7e45eadb29615fa9d09fada140de1c38c7", size = 420090, upload-time = "2026-06-11T04:15:12.868Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f7/5ea755a89868c04f9cdf6d96d2d99da4b3d198af10e76a6082dd0fceccc0/msgpack-1.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d16e1f2db4a9eebc07b7cc91898d71e710f2eed8358711a605fee802caff8923", size = 378538, upload-time = "2026-06-11T04:15:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/126e59332a439c94ffd682c38ca0102b23480e2784b3dac48d8959b0bbac/msgpack-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb2e700e85f1e27bbb5c9de6cc1c9a4bc5ac64d5404bdcbcb37a0dc7a947a3", size = 399468, upload-time = "2026-06-11T04:15:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/da/f9/7abcef683a0ad2e5ab3a4940344aad9f20cdf1f42057ecb0982cf55085d6/msgpack-1.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:717d0b166dd176a5f786aeafff081f6439680acf5af193eb63e6266c12b04d3d", size = 374212, upload-time = "2026-06-11T04:15:17.536Z" }, + { url = "https://files.pythonhosted.org/packages/27/23/2d62cf0e971678e96f8a3cfa9bd77fb719ddb98da73790f63c53fd847ad8/msgpack-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e87c7a21654d18111eb1a89bd5c42baba42e61887365d9e89585e112b4203f9e", size = 414361, upload-time = "2026-06-11T04:15:18.99Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/f5c153f614037aaf802d291a4653ba1bb731f56feacba886f7c21c109e56/msgpack-1.2.0-cp312-cp312-win32.whl", hash = "sha256:967e0c891f5f23ab65762f2e5dc95922759c79f1ef99ef4c7e1fdd863e0d0af9", size = 64389, upload-time = "2026-06-11T04:15:20.237Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/8aafce6e5544b43b84cb670aca40c8bea7eb5ae8f42bfcbdc7098739987a/msgpack-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:6c23e33cee28dcffa112ae205661da4636fd7b06bd9ad1559a890623b92d060b", size = 71185, upload-time = "2026-06-11T04:15:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/ba/08/9cc94be1fc1fe3d1379d439326259aef0344274f64623a8138feb54dff68/msgpack-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eeb771571f63f68045433b1a35c0256b946f31ed62f006997e40b8ad8b735af", size = 64481, upload-time = "2026-06-11T04:15:22.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/26/2902c6946ab5c8fe1e46e40842dfc32b8824464ad5cd4725364fd83f7a58/msgpack-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a1d30df1f302f2b7a7404afbac2ab76d510036c34cf34dffb01f704a7288e45", size = 82621, upload-time = "2026-06-11T04:15:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/c9/59/7e6b812629d2f919e586041bffc130e1af32079f71bb20699eed54ed6d92/msgpack-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:581e317112260d8ca488d490cad9290a5682276f309c41c7de237a85ed8799c8", size = 81866, upload-time = "2026-06-11T04:15:25.032Z" }, + { url = "https://files.pythonhosted.org/packages/31/13/8c291196e60aafdbae38f482205d79432297749ac5d412fe638154fb6f1d/msgpack-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6827d12eacc16873eba62408a1b7bbe8ecfb4a8f7ed78a631ae9bae6ad43cf2", size = 405618, upload-time = "2026-06-11T04:15:26.235Z" }, + { url = "https://files.pythonhosted.org/packages/fb/63/68f5d0ea81e167db5f59ddb94dc6f837667062113feff1c73fabf8907061/msgpack-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a186027e4279efa4c8bf06ce30605498d7d0d3af0fba0b9799dce85a3fd4a93c", size = 416468, upload-time = "2026-06-11T04:15:27.732Z" }, + { url = "https://files.pythonhosted.org/packages/73/58/567dddf5c5a2790f673bcd7d80c83466d68e5ee9a9674ebca3db8101c0c8/msgpack-1.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a96142c14a11cf1a509e8b9aaf72858a3b742b7613e095ce646913e88ce7bd99", size = 374464, upload-time = "2026-06-11T04:15:29.286Z" }, + { url = "https://files.pythonhosted.org/packages/0d/30/0c2342fc9092e4498045f5f60bca6ccbe4f4d87789778c2300e6fd6efe82/msgpack-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50c220579b68a6085b95408b2eaa486b259520f55d8e363ddc9b5d7ba5a6ac6d", size = 395879, upload-time = "2026-06-11T04:15:30.973Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/9565b29b58ce3c33e177b490478b7aaeb8f726ecaaeda26d815893c1db5a/msgpack-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4dcb9d12ab100ecacdfaaf37a3d72fe8392eacc7054afc1916b12d1b747c8446", size = 371749, upload-time = "2026-06-11T04:15:32.418Z" }, + { url = "https://files.pythonhosted.org/packages/f2/da/7bade19d60b73e2ef73fb76aaf4504c112a70cb760951b7202a0c64b5111/msgpack-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a804727188ab0ebb237fadb303b743f04925a69d8c3247292d1e33e679767c15", size = 410416, upload-time = "2026-06-11T04:15:34.053Z" }, + { url = "https://files.pythonhosted.org/packages/6d/14/c0c619571c02432208a5977a8dbdd3fc65fe1369f8226ca4b6d08cca87d8/msgpack-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1a1ac6ae1fe23298f79380e7b144c8a454e5d05616b0096584f353ba2d750114", size = 64357, upload-time = "2026-06-11T04:15:35.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/a5/de06718460909aa965737fec4cfe8a15dedc6544a8c55feeb6956fa0d6e3/msgpack-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c3c80949d79578f9dc85fd9fb91edfe6694e8a729cd5744634d59d8455fdde3", size = 71057, upload-time = "2026-06-11T04:15:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/c7/52/73446b0141c94a856e22b787c56709c0815fc34f185326577e15b26d8cfe/msgpack-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fcf8f76fa587c2395fd0057c7232dbf071241f9ad280b235adb7ab585289989e", size = 64490, upload-time = "2026-06-11T04:15:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/35/3d/a7e3cdafa8c0cf36c81e2fa848ec4d30cf089459af45b390ad03f9ce6f49/msgpack-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f854fa1a8b55d75d82ef9a905d9cdbeffdf7897c088f6020bd221867da5e56a5", size = 83032, upload-time = "2026-06-11T04:15:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/53ddfba0e347cc4b484e95f629c5850b9e800ca8390c91ffc604407acf87/msgpack-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e90df581f80f53b372d5d9d9349078d729851a3a0d0bd74f53ccb598d01e45b8", size = 82600, upload-time = "2026-06-11T04:15:40.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/fd/e64c2c776e6dbad0af3c963fe0c0dd1ee1ba09efac478b233ab1db41868f/msgpack-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b276ed50d8ac75d1f134a433ae79af8557d0fa25ee5b4737da533dfc2ce382e8", size = 404342, upload-time = "2026-06-11T04:15:41.87Z" }, + { url = "https://files.pythonhosted.org/packages/1b/60/fb9a08e6ccba882dfd370a5837fe3a07572938fdfe954f0f17fdf3e574b9/msgpack-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:544d972459c92aa32e63b800d07c2d9cf2734a3be29cee3a0b478a622850e9f5", size = 412351, upload-time = "2026-06-11T04:15:43.253Z" }, + { url = "https://files.pythonhosted.org/packages/37/4d/df5c575c274fedc68ac9c6c61d045161899efad2afcdc25138efa7edde69/msgpack-1.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a070147cc2cf6b8a891734e0f5c8fe8f70ed8739ab30ba140b058005a6e86af4", size = 373331, upload-time = "2026-06-11T04:15:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a4/c8b98f8191e985ed2003d87664ce3c95cca41db5d0cf6bf4f54327d32ec8/msgpack-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7685e23b0f51745a751629c31713fbefdef8896b31b2bb38299dfa4ae6c0740c", size = 394654, upload-time = "2026-06-11T04:15:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/d4/49/76f036720a602ea24428cfec5ec806f2487c0380b1bff0a2aa3094e15f87/msgpack-1.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b9204daeee8d91a7ae5acf2d2a8e3983be9a3025f38aa21bfaefbd7eea84a7dc", size = 370624, upload-time = "2026-06-11T04:15:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/9f/38/40af3d29232833705a43b0fce0d07425cc280a7b92ab2b29932425b40df4/msgpack-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bfc057248609742ebbabf6bcd27fea4fd99c4980584e613c168c9b002318298f", size = 408038, upload-time = "2026-06-11T04:15:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/30/b2/f140ca450524dff4d8d0eb81eb9ed75f8f3e0b1f12e49c5b01617cfa0b1c/msgpack-1.2.0-cp314-cp314-win32.whl", hash = "sha256:a3faa7edf2388337ae849239878e92f0298b4dab4488e4f1834062f9d0c410c9", size = 65823, upload-time = "2026-06-11T04:15:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/4d/13/6517bf966b841c7675ded30701a068ce141f3e698a27aaa35c702d8e078b/msgpack-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a3effc392a57744e4681e55d05f97d5ee7b598747d718340a9b4b8a970c40e1", size = 72484, upload-time = "2026-06-11T04:15:52.289Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/1d948420fdaa24de4efdb8012a6a5bebe09c82ee002b8c2ca745e9917f1f/msgpack-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:56a318f7df6bec7b40928d6b0519961f20a510d8baabf6baa393a70444588f0a", size = 66657, upload-time = "2026-06-11T04:15:53.583Z" }, + { url = "https://files.pythonhosted.org/packages/39/16/1674faa1b7bddc19e79b465fd8e88e2cf4e3f7cae90723740701e8541068/msgpack-1.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:afa4a65ab2097795e771a74a3a81ea49534aaeba874eaf426a3332268e045ae6", size = 86093, upload-time = "2026-06-11T04:15:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/dd/24/f241bcfdd9e96b2246289357c5a5e5a496189fd41c5844bee802c116aac7/msgpack-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:409550770632bb28daa70a11d0ed5763f7db38f40b06f7db9f11dd2794d01102", size = 86372, upload-time = "2026-06-11T04:15:56.381Z" }, + { url = "https://files.pythonhosted.org/packages/94/c9/57f8ab98a1b21808c27b6dd6029053e0a796ffbb9b371e460dbe997011a9/msgpack-1.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf47e3cd11ce044965a9736a322afdd390b31ed602d1c1b10211d1a841f1d587", size = 428207, upload-time = "2026-06-11T04:15:57.739Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/4fd4aa739f131ded751ca7167c8ee87d2aab32506ebbeea893b60b51d343/msgpack-1.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:204bc9f5d6e59c1718c0a4a84fc8ff71b5b4562faac257c1a68bca611ecf9b72", size = 426082, upload-time = "2026-06-11T04:15:59.356Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/db88e9a08fcd6513decaad06cbd5c168142bc3e662fb2f1aca3a563b7aa1/msgpack-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:610154307b27267266368bc1d1c7bb8aeb71da7be9356d403cb2442d9e6399f5", size = 378355, upload-time = "2026-06-11T04:16:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/eee4dd703d7a600cf46159d621c070b0b9468cf3dbade4ea8272bf5232a4/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6799f157bb63e79f11e2e590cfdb28423fc18dd60c270c3914b5b4586ae36f7e", size = 410848, upload-time = "2026-06-11T04:16:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/0a/195e2c549fd4631eb7f157d016ff15a10c4c1cf82b6d0a9b1edaef5174b1/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:72bd844902cf0a5ac3af2ef742f253cd0b1e5bcd184f49b4fb9a6a1f7bf305e8", size = 376152, upload-time = "2026-06-11T04:16:04.041Z" }, + { url = "https://files.pythonhosted.org/packages/45/9b/bdd143fa79baec411dc658f5686fed680a18b36fcea5fccb6af1b8c7d832/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd450f78d0d81722c80da6cdbf674a856967870a9db2f6c4debc4d8b3c67c", size = 417061, upload-time = "2026-06-11T04:16:05.63Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/011ffcd8b919f55196ec53f12ae162e21c879d95afba226894314ff62c07/msgpack-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:378caf74c4c718dfc17590ce68a6d710ed398ff6fcf08237de23b77755730b55", size = 70782, upload-time = "2026-06-11T04:16:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/57/a8/9b8791ca96b1be6b9f659c718271e2cb7f99f73f58aad2dd0b30f750f6c0/msgpack-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:553b42598165c4dd3235994fd6e4b0dfb1ce5f3fd33d94ba9609442643015f38", size = 77899, upload-time = "2026-06-11T04:16:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" }, +] + +[[package]] +name = "narwhals" +version = "2.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, +] + [[package]] name = "networkx" version = "3.6.1" @@ -1072,6 +1309,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "parakeet-mlx" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dacite" }, + { name = "huggingface-hub" }, + { name = "librosa" }, + { name = "mlx" }, + { name = "numpy" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/1a/d7ab33b3e25dcf2d97d0192fd1e718f2c6e69b5775370413e144fef54256/parakeet_mlx-0.5.2.tar.gz", hash = "sha256:6c6ba3c4d42ebf015dde515e5a83858a516add3f5b2c6bd4a7be5c4ba8bd4d02", size = 37791, upload-time = "2026-06-05T07:52:35.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/54/09a3501c74f1ef54a36002534661cb89a0b2f098fcad87fe64f515a0dba7/parakeet_mlx-0.5.2-py3-none-any.whl", hash = "sha256:50afb6ddb62237a6486e214482c25ef12759832fda3cd514e159938fa5970d9c", size = 37353, upload-time = "2026-06-05T07:52:34.452Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1111,6 +1365,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -1156,16 +1439,18 @@ wheels = [ [[package]] name = "pyobjc-core" -version = "12.1" +version = "12.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383, upload-time = "2026-06-19T16:19:39.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, - { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, - { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/16564ef5e4568ee0edd9e712d8111dc8b67621d6bb6ff430646ee2d637dd/pyobjc_core-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24b76a63caf0b5369d4a377c7c0438cd70df81539057af3db839bfaa3579e04a", size = 6484662, upload-time = "2026-06-19T16:04:44.979Z" }, + { url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347, upload-time = "2026-06-19T16:04:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/b9b0ddffae66996b8779f1f7958adc9f21c13a0448cd3be8d7fe589b5b0f/pyobjc_core-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af101222762665a4125157906cb4b23f5d5a63d3851d5e0504f72a1eaaa2cfd2", size = 6436004, upload-time = "2026-06-19T16:04:53.257Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/bd309ede07784c6e5fac4b440c90a5f72a66da7859ed303a9392fe8a5f3f/pyobjc_core-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:efe465e3ecc6fc73f7c7622620345d134a8d34564ab1c29d8247e45f4ed55071", size = 6687044, upload-time = "2026-06-19T16:04:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8a/cfa4f56939d554dbb342ec6e5226a441e2f552bc2002a0ddf7705bb11bef/pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a", size = 6429289, upload-time = "2026-06-19T16:05:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/42/74/446c89bc18103aaa4a00d1fb85ff8acace9a0dc3f362d9678ebf7571e275/pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7", size = 6690181, upload-time = "2026-06-19T16:05:06.201Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/0121ee4c616af07ad2de8cd1a286f6978dc9a227eb58b7c2e875cb68a1df/pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e", size = 6487078, upload-time = "2026-06-19T16:05:10.093Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a8/cb9fcc150f97d0bf22a2028f88b24cc35949beb1bcc7b8bc5c17d4401677/pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb", size = 6733064, upload-time = "2026-06-19T16:05:14.313Z" }, ] [[package]] @@ -1188,21 +1473,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/bb/dab21d2210d3ef7dd0616df7e8ea89b5d8d62444133a25f76e649a947168/pyobjc_framework_applicationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f72e20009a4ebfd5ed5b23dc11c1528ad6b55cc63ee71952ddb2a5e5f1cb7da", size = 33238, upload-time = "2025-11-14T09:36:24.751Z" }, ] +[[package]] +name = "pyobjc-framework-avfoundation" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/26/7616f0bc8e4eaaba948cf5d220c8f55e0f54f617a2812392a82f19c30f39/pyobjc_framework_avfoundation-12.2.1.tar.gz", hash = "sha256:2735e4f1c345d2b533541577e292f3ad2f75d19200eff99f1a2db16d78b4f1a3", size = 410329, upload-time = "2026-06-19T16:19:52.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/88/38ccef918dea4188e0001e644f3bc16e26d11959b04838e5e243798f703a/pyobjc_framework_avfoundation-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce4b2bb2ae51ad5c27f1d2f2a623bef8b443bccc34423b5149df010f9f501c", size = 85536, upload-time = "2026-06-19T16:06:21.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/e427b2fd705e9dabbfa0ab89b863305788906e34064db2bb930f1c3ba216/pyobjc_framework_avfoundation-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f372800274df35f8d964cad0ad6da7636f1b6720e802e28e5a6bc1f16f16f135", size = 85578, upload-time = "2026-06-19T16:06:22.292Z" }, + { url = "https://files.pythonhosted.org/packages/98/f5/b38da31d9f95770d0f0f9b9724a898d240d6fb630796e04e9682384d086c/pyobjc_framework_avfoundation-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9291848b0f0f66031bd3af2549e1e0cdf43e4872b9c562bd29f0239c6ef2b75f", size = 85628, upload-time = "2026-06-19T16:06:23.317Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a4/f1bc5fc239015d8fb4414a83592d69b85b8bbdc68f2403c21dbcdcb8ce12/pyobjc_framework_avfoundation-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3e92a8502d389469b1307a6a2c386f20d2e77878c7e35e3e14120596b47eb205", size = 86077, upload-time = "2026-06-19T16:06:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a2/4a65d749d57dd16b034ee0935b7daa1228e8a735bf920e9eab9f4aaa9f1e/pyobjc_framework_avfoundation-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:043aedd86adbc0a3bfe8f76d1a1ce920539e47b5dd5d965358f93e0063c9a53d", size = 85836, upload-time = "2026-06-19T16:06:25.377Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/b9e8f51821a9e5a4a76bea7ab10c2dc1b51b8a141cf4bccbba022f2db19f/pyobjc_framework_avfoundation-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50e2b65e5c3845eb8ad0d37868a7f040656fe69e6c03f4282670e7dc52ed7a4a", size = 86173, upload-time = "2026-06-19T16:06:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3d/eda090c53a98293d6ad78eb2a0639e9ab402c14555c2d9e3c311fc482051/pyobjc_framework_avfoundation-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:3f74e199c933df54020b972b985af5a5d2e63134278ea02dfd81923976500794", size = 85889, upload-time = "2026-06-19T16:06:27.517Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/88126080195ab3fc95b08562ce30f9f599bdabf282b3a4e57bd2c8c2595c/pyobjc_framework_avfoundation-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1ec2fdfe6eee51f12e7d933e0109ea8ad553acf4301c01f46b79d0a9d378d705", size = 86232, upload-time = "2026-06-19T16:06:28.535Z" }, +] + [[package]] name = "pyobjc-framework-cocoa" -version = "12.1" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751, upload-time = "2026-06-19T16:20:05.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/d6/dc66ea8519a0475efbccf73f82cc28066339bb300a27f5e1bf91ab1d7002/pyobjc_framework_cocoa-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dc6da84f4fc62cc25463bbb85e77a57b8d5ac6caf9a60702daf2edb601332f15", size = 387298, upload-time = "2026-06-19T16:07:37.412Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113, upload-time = "2026-06-19T16:07:38.9Z" }, + { url = "https://files.pythonhosted.org/packages/cc/46/68e8e4d926a2f70fed0437047bc3f9fe08af8fe620d94d80656ebc3cfa9b/pyobjc_framework_cocoa-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3b74a78fa7803e547b32e5e8ec1b49987b52fe318383e793bc6cd49b80efbd9f", size = 388183, upload-time = "2026-06-19T16:07:40.483Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f3/dfc9af4c9eb2e5389c860ad5ef252be9fe456db09f39d537555dc5057aa1/pyobjc_framework_cocoa-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dc2eaca2f13c7bcd8e41e51a372e47825dea9dd3126108760eed7ba883d2945c", size = 392275, upload-time = "2026-06-19T16:07:42.078Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c8/b90baa8f3592eded79b4be98fb59d2b8dc16b62361e34292bd95806ebd9f/pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561", size = 388357, upload-time = "2026-06-19T16:07:43.364Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/64a94651b9294702d55e748d94de30e25bc59d0784526be7643f4467eccd/pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b", size = 392404, upload-time = "2026-06-19T16:07:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/26e8a7bf1f5e8caa38b7f80d486296f9fd3c97e71ad7e5444ef22e802758/pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103", size = 388589, upload-time = "2026-06-19T16:07:46.276Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eedf743a303ea742b8e082afe3613fb4d6618bc1a48cf2568b004ce906f7/pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1", size = 392691, upload-time = "2026-06-19T16:07:47.477Z" }, +] + +[[package]] +name = "pyobjc-framework-coreaudio" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/df/f1d402bb7b437374f942bb19410a955a74291867c77a920d9c13887d9e48/pyobjc_framework_coreaudio-12.2.1.tar.gz", hash = "sha256:7dfbf1851523aed453af43a628e057d8950d6e020574aa497a2e4f559b6383c8", size = 78690, upload-time = "2026-06-19T16:20:10.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/1f/853c498e18db8b6d9eb0cb2261bf8cda3948f2c925d1c820baaf96fdb54d/pyobjc_framework_coreaudio-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:214ce41d1ac3743377f607326d3ff70494dd6f75b2c575fe9c99d3890c545dee", size = 35149, upload-time = "2026-06-19T16:08:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c1/1fc7a344ac15646fdf78c91af81028113c87375036e505082cf1f90bd657/pyobjc_framework_coreaudio-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:41a0d8c3b3957f35f17370071aeef94e176d07c9e2130d0aeeb4b260117acd52", size = 35415, upload-time = "2026-06-19T16:08:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/71b2e3bd03f0404c89b432273d272dc5427185fd9ed828036730bfc9d057/pyobjc_framework_coreaudio-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6fb46bc090edcaefeb70be2c179071e7a758e3375aebd3338b11773e371c3dce", size = 35445, upload-time = "2026-06-19T16:08:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/67/5b/07dc0bf9f79d3b76effba3f0a754be6fda0947848939236820c8e70ac896/pyobjc_framework_coreaudio-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d05e5fd58c1e5f48e7767b1f6182cf36ee9bff38568544d51f468b25ad90e334", size = 38205, upload-time = "2026-06-19T16:08:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a4/6495dd34b1ed7a0e8f6d672577da62a92b166a0bc2ded8e55e552cbe5ad2/pyobjc_framework_coreaudio-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:efb79709371f29139f44adc36bd6c32d5a3a0ff3ffd0c5f3f372275cef893b18", size = 36697, upload-time = "2026-06-19T16:08:14.407Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9e/9f0cba5b973a5c8041edb75cf0870f40324437721428b1baceb437553e3f/pyobjc_framework_coreaudio-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9c06b9d7ee3b2a76dd171de519f571d09bdebd85f40b5960d16f711395513cdd", size = 38300, upload-time = "2026-06-19T16:08:15.297Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/e8df283cdd73dec1b97033be04a89a05e17714c8e2229f2a2e5a44f377c7/pyobjc_framework_coreaudio-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:51be22e4fac73ba4b26ee34c457ac94f56218e4e9c1494f8f4e7dfe9dbb6a15f", size = 36723, upload-time = "2026-06-19T16:08:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/c76ddff169b45d7239468f44bf4af3ff431c014fbf0aa5dbbfcc099672f8/pyobjc_framework_coreaudio-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:18717e6e106556fd4ffd0bdf6a6dae796fc9100c6b373e664437604dd0b9e94d", size = 38333, upload-time = "2026-06-19T16:08:17.264Z" }, +] + +[[package]] +name = "pyobjc-framework-coremedia" +version = "12.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/79/f501d730a9c320e0b2b3916e95f57e66dd6736d210a1aa5b63eb6c43e605/pyobjc_framework_coremedia-12.2.1.tar.gz", hash = "sha256:71b45f7cd52bd997d836c15a0e1016db90815a219dc87fd20435a6f08b87df7b", size = 98252, upload-time = "2026-06-19T16:20:15.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, - { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, - { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/8b97b76f0643e01c98d5c246b1ed74884f4b3c43dfff2a36886212e20dfa/pyobjc_framework_coremedia-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:38e03a92a7fdf43162faadf354a8497ae764e3b3be26a8ffb107ed5d743cac76", size = 29513, upload-time = "2026-06-19T16:08:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/af/17/6bf365530573a6b7719b5a47efe6c38ac8c42f6b556babe419f5de48a84c/pyobjc_framework_coremedia-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fd0e25bc704dd91882bc29d682880c42f712b39bdfe3d3168780d24a9d9eaf26", size = 29419, upload-time = "2026-06-19T16:08:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2a/9e5beae2961c9d22ce16258f39edae69996ee0167dd4cfe4e771454086c1/pyobjc_framework_coremedia-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:45878f686ce8ea1735ce382b34ef3a5852cafe0ae2a27a49c08701f4c3ab830b", size = 29431, upload-time = "2026-06-19T16:08:54.41Z" }, + { url = "https://files.pythonhosted.org/packages/dc/51/00b7f012e55475502296cc8ef276b94ad7d4d10d97ce2e88bf9d87f9b664/pyobjc_framework_coremedia-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9f2b30ef288c9f2fa1599a1cc5868f8cd949c7138a7f15244c7a6884afcef273", size = 29491, upload-time = "2026-06-19T16:08:55.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/dc/7083e6781fcd843d210ec6d247d28c6e1032ea1c5655b9e1182d0a85f331/pyobjc_framework_coremedia-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:84e9fb3f7b6420fc3865a5812604c63546b5777f4999e0e0e830a9a0816e8743", size = 29468, upload-time = "2026-06-19T16:08:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/11/74/edb3ec87d2e5f962c75af990207e4df39b290f11b4f4c3cc446ee141e4e7/pyobjc_framework_coremedia-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:541d64d2fc7576054bdc3508a94f37671e81e5ca1582e16ebec2c0e05dd75412", size = 29519, upload-time = "2026-06-19T16:08:56.956Z" }, + { url = "https://files.pythonhosted.org/packages/28/0f/9176b6a7666e46273c0392c8f77bc812e006dc74a1c21263c4bdf385012f/pyobjc_framework_coremedia-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:2a355770b66b4caf99560c11af44dfeca53066ece75807fc6a391a16369ccc72", size = 29502, upload-time = "2026-06-19T16:08:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/13e7d8d16528734690f83abd901cbc53dd6f7c1adbcb3a902166735c6c21/pyobjc_framework_coremedia-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:5536453ba631227810a1141fb1761149244bd6d0e7cd4b12477be3b164f5bb11", size = 29555, upload-time = "2026-06-19T16:08:58.792Z" }, ] [[package]] @@ -1226,20 +1576,22 @@ wheels = [ [[package]] name = "pyobjc-framework-quartz" -version = "12.1" +version = "12.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/2a8b84dbf1fe7c04dd96ea73d991678d4e09a909f51971ecc51629bb2ab4/pyobjc_framework_quartz-12.2.1.tar.gz", hash = "sha256:b3b8b6f71e66147f8ff9e6213864cc8527e3a0b1ee90835b93ce221f4802d9b0", size = 3215521, upload-time = "2026-06-19T16:21:30.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, - { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, - { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/527d1ff856e2f2446b5887be01989cc08f9adaf3de7d4eb13d07826c362f/pyobjc_framework_quartz-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60f29408b4f9ed5391a29c6b63e2aa56ddfb8b66b3fb47962930427981e14462", size = 217998, upload-time = "2026-06-19T16:16:02.978Z" }, + { url = "https://files.pythonhosted.org/packages/14/fc/d7c7b3134cdbd1a487f3f77b5be125d87a6c9e7d9411035739d99335cc0c/pyobjc_framework_quartz-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:de9c8cca7e95290c8d540466af11c7cdfe3a5458e6f56c34006d5b45243f9ed9", size = 219000, upload-time = "2026-06-19T16:16:04.29Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4b/861f91a1565d3189ee899e177b915551fb9a7e2ca25414025a8974f04e74/pyobjc_framework_quartz-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54c9bc7f507192691841ee4eba5bf36990b259df83ac728efed2d7ea1cd021e4", size = 219403, upload-time = "2026-06-19T16:16:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/b27010d2f288737f627f74be6d5549f49c841542365c84b9a3011fe39ce7/pyobjc_framework_quartz-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bfc0d2badd819823d21df8069dcf9544ce360ed747a8895c51bdb25d8d125f45", size = 224458, upload-time = "2026-06-19T16:16:07.252Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/85ffd9d433989205d572a50d625c63b29c05e0c5235a725f15ae1023672c/pyobjc_framework_quartz-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ceb56939c337b36d9d81185ade31f77dc52c85cf79bb16e53e9b32f54b6bb3f5", size = 219769, upload-time = "2026-06-19T16:16:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d6/b917e4b63d72ea84a27121076f3033f23f6497c0e6ce8d304766c899897f/pyobjc_framework_quartz-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8105c98b798f2bf81c05c54bddeeadbf62f0b5dfec13bd6e719dd2cdf7e1cddf", size = 224717, upload-time = "2026-06-19T16:16:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/04/e2/f3c1ed3228f7430ef5ade23db6f1fcbae99290f177ce5653348fd9e05f4d/pyobjc_framework_quartz-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:bbc214f1a216b5d3651bc832d0ac4589f029f3f37cd6cbb370aac12a7c77942c", size = 219825, upload-time = "2026-06-19T16:16:11.433Z" }, + { url = "https://files.pythonhosted.org/packages/66/2a/2c99a5ad2fe0a11600ea123b8e9a08ff138fcb2ad1e13e376f4bd4aa1d96/pyobjc_framework_quartz-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:ca61624a0b0e6286d8a0f97f47eb9011e4e81e9a339db436d48af527e7065bb1", size = 224770, upload-time = "2026-06-19T16:16:13.035Z" }, ] [[package]] @@ -1473,6 +1825,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + [[package]] name = "scipy" version = "1.16.3" @@ -1557,6 +1978,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -1599,6 +2076,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/c7/16123d054aef6d445176c9122bfbe73c11087589b2413cab22aff5a7839a/sounddevice-0.5.3-py3-none-win_amd64.whl", hash = "sha256:f55ad20082efc2bdec06928e974fbcae07bc6c405409ae1334cefe7d377eb687", size = 364025, upload-time = "2025-10-19T13:23:56.362Z" }, ] +[[package]] +name = "soundfile" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" }, +] + +[[package]] +name = "soxr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/11/27cebce4a108f77afea7c80545115536b45e3f11ebfb914f638fdd9ba847/soxr-1.1.0.tar.gz", hash = "sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44", size = 173067, upload-time = "2026-05-03T00:15:18.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/49/3e6bc84f87439f222f40b616e9a29a170f41fb564710ea510df19dc26907/soxr-1.1.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:34cc92208c3c412c046813e69da639c04a792c6a41fbfd7d909d359cd3e97a2d", size = 205699, upload-time = "2026-05-03T00:14:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/2f/94/216f46096a85b07d1e6ba7fd44491402e912a3d688cd4f36f0a600ca155f/soxr-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd30f7201eac896ebf5db7b09156e6f1a1b82601900d29d9c8449bdad8365b11", size = 167381, upload-time = "2026-05-03T00:14:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/cb/06caa463b8181ec1981bd6376d4a873748b7008193188b8cfb60391eb131/soxr-1.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1577865e993f98ffb261257c3060fa76ec3db44ed3f181b16464268000424464", size = 210938, upload-time = "2026-05-03T00:14:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/d5964551ca818b7f0c7ef7f3899056263b60ef098a801066350a9672ca8f/soxr-1.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3da87e3ffa3e41823d873b051c7ecb2acebd8d1b6b46b752f5facf10a0d84ab9", size = 245268, upload-time = "2026-05-03T00:14:51.422Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/371467eb86c7ba6810df0bfe9409bcd9c52ec5615b111190fafe23e4d2e1/soxr-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae30c48ac795378cf23ba3c7c640b8ff794af714ac388b9fd6b31a40b39e6e86", size = 176779, upload-time = "2026-05-03T00:14:53.09Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f3da7973b5f1b05d2d7e94d5376b881dcbc05297900cae6c3d33d95b209b/soxr-1.1.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da", size = 204124, upload-time = "2026-05-03T00:14:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/200013a74641f8774664bbcd2346c695c05c2e300ea792adcb40a293eed0/soxr-1.1.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4", size = 165457, upload-time = "2026-05-03T00:14:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/2e5eba817a762a2ec589ff165b8bc5955b25a0ad140045f7cd8e45410543/soxr-1.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06", size = 206529, upload-time = "2026-05-03T00:14:57.646Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/0e55195893228609c9a08c3b13b7a83a46c3a992cd00d3304f0f320cfb07/soxr-1.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1", size = 240413, upload-time = "2026-05-03T00:14:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/621e4150e4815246ad552d215a8a294a90143fedd19ee442cf82d3b3abc8/soxr-1.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4", size = 174357, upload-time = "2026-05-03T00:15:00.724Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/77b74f1e95af0e11e52e9a034421aece7f7b45afd15a909afd41d5a5d102/soxr-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a941f5aaa0b8abced24318105c1ea22576afcc1138c19f625716ce4e2f76ad64", size = 207990, upload-time = "2026-05-03T00:15:02.1Z" }, + { url = "https://files.pythonhosted.org/packages/30/86/600cc31f982288167a59972746f117790162012546f995a32b5a55394b16/soxr-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:feebcba99ac99adb8009d46c8f4c1956b8c167576b0ae8a6fb47502e9a6f78e7", size = 169288, upload-time = "2026-05-03T00:15:03.75Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/80cd9aae0645513db1076d4384e8b2d895faf5009218b4a04348012c54fc/soxr-1.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52c9ca84e3dc656d83acc424574770e20ea8e0704dc3842d4e27b0fe9d3ba449", size = 211405, upload-time = "2026-05-03T00:15:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/cc3c80ac9b2289da4cf46c5d53b05e4327e6f5560a25868d06f9e2213af1/soxr-1.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4977323ef9c3aa3c2a26ff5fe0191c84b8fd759daf7afb1f25a91a55ad8b730", size = 244617, upload-time = "2026-05-03T00:15:07.134Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/f7af5fae841ffe32ed8440234ea2ad6adecca3bd92b6101076268c429000/soxr-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e17d4ef9b0185214b2c0935605ae63f827ea423bc74964be44763d68d2b6c21e", size = 187253, upload-time = "2026-05-03T00:15:08.813Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts" }, + { name = "standard-chunk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-sunau" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ae/e3707f6c1bc6f7aa0df600ba8075bfb8a19252140cd595335be60e25f9ee/standard_sunau-3.13.0-py3-none-any.whl", hash = "sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622", size = 7364, upload-time = "2024-10-30T16:01:28.003Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1611,6 +2169,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -1665,6 +2232,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + [[package]] name = "tomli-w" version = "1.2.0" @@ -1751,6 +2344,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "transformers" +version = "5.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/7c/8240f612819718100a9346dc28dea6a11370c3ca9c8c6eabadd3dea4ef29/transformers-5.12.1.tar.gz", hash = "sha256:679ee731c8225347889ad4fb3b2c926a62e9da3b7d284e9d12c791da7272466b", size = 8924054, upload-time = "2026-06-15T17:27:50.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/56/bbd60dd8668055803bf8ba55a81f9b8a8b31497f620109a9671d26a2076d/transformers-5.12.1-py3-none-any.whl", hash = "sha256:2a5e109d2021265df7098ffbb738295acaf5ad256f12cbc586db2ea4dcbb1a8a", size = 11150587, upload-time = "2026-06-15T17:27:46.679Z" }, +] + [[package]] name = "triton" version = "3.5.1" @@ -1774,16 +2387,18 @@ wheels = [ ] [[package]] -name = "typer-slim" -version = "0.20.0" +name = "typer" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, - { name = "typing-extensions" }, + { name = "rich" }, + { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] [[package]] @@ -1858,36 +2473,49 @@ wheels = [ [[package]] name = "voice2text" -version = "0.2.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "loguru" }, - { name = "mlx-whisper" }, + { name = "mlx-lm" }, { name = "numpy" }, + { name = "parakeet-mlx" }, { name = "pynput" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, { name = "scipy" }, { name = "sounddevice" }, ] +[package.optional-dependencies] +whisper = [ + { name = "mlx-whisper" }, +] + [package.dev-dependencies] dev = [ { name = "hatch" }, + { name = "mlx-whisper" }, { name = "ruff" }, ] [package.metadata] requires-dist = [ { name = "loguru" }, - { name = "mlx-whisper" }, - { name = "numpy" }, + { name = "mlx-lm" }, + { name = "mlx-whisper", marker = "extra == 'whisper'" }, + { name = "numpy", specifier = "<2.4" }, + { name = "parakeet-mlx" }, { name = "pynput" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, { name = "scipy" }, { name = "sounddevice" }, ] +provides-extras = ["whisper"] [package.metadata.requires-dev] dev = [ { name = "hatch" }, + { name = "mlx-whisper" }, { name = "ruff" }, ] diff --git a/v2t/__init__.py b/v2t/__init__.py new file mode 100644 index 0000000..5ece064 --- /dev/null +++ b/v2t/__init__.py @@ -0,0 +1,3 @@ +"""v2t — local, MLX-first voice-to-text with LLM cleanup.""" + +__version__ = "0.3.0" diff --git a/v2t/__main__.py b/v2t/__main__.py new file mode 100644 index 0000000..eb53e2f --- /dev/null +++ b/v2t/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +raise SystemExit(main()) diff --git a/v2t/app.py b/v2t/app.py new file mode 100644 index 0000000..04def87 --- /dev/null +++ b/v2t/app.py @@ -0,0 +1,526 @@ +"""The push-to-talk engine: record on hotkey, transcribe, clean up, paste. + +macOS-only at runtime (native pasteboard, System Events paste, global hotkey). +""" + +from __future__ import annotations + +import os +import queue +import signal +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path + +import numpy as np +import sounddevice as sd +from loguru import logger +from scipy.io import wavfile + +from . import backends, config, permissions +from .config import Config + + +MIC_PANE = "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" + + +def _resolve_hotkey(name: str): + from pynput import keyboard + + keys = { + "cmd_r": keyboard.Key.cmd_r, + "cmd_l": keyboard.Key.cmd_l, + "alt_r": keyboard.Key.alt_r, + "alt_l": keyboard.Key.alt_l, + "ctrl_r": keyboard.Key.ctrl_r, + "ctrl_l": keyboard.Key.ctrl_l, + } + if name not in keys: + raise SystemExit(f"unknown hotkey {name!r}; choose: {', '.join(keys)}") + return keys[name] + + +def check_and_request_permissions() -> None: + """Fail early with the exact macOS permission panes that still need a grant.""" + logger.info("Checking permissions...") + states = permissions.statuses() + if states["microphone"] == "not-requested": + logger.info("Requesting Microphone permission...") + states["microphone"] = ( + "granted" if permissions.request_microphone() else "denied" + ) + checks = [ + ("Microphone", states["microphone"] == "granted", "Privacy_Microphone"), + ( + "Accessibility", + states["accessibility"] == "granted", + "Privacy_Accessibility", + ), + ] + missing = [(name, pane) for name, granted, pane in checks if not granted] + if missing: + names = " + ".join(name for name, _pane in missing) + message = f"Grant {names} to the launching app, restart that app, then start v2t again." + config.write_last_error(message) + logger.error(message) + security = "x-apple.systempreferences:com.apple.preference.security" + for _name, pane in missing: + subprocess.run(["open", f"{security}?{pane}"], check=False) + raise SystemExit(1) + logger.success("Permissions OK") + + +class VoiceToText: + def __init__(self, cfg: Config): + self.cfg = cfg + self.stt_model = cfg.stt_model or backends.STT[cfg.backend].default_model + self.stt = None # loaded in run() + self.cleaner = None # loaded in run() if cleanup is enabled + self.recording = False + self.processing = False + self.frames: list[np.ndarray] = [] + self.stream = None + self.record_start = 0.0 + self.was_playing = False + self._warned_mic = False + self.jobs = queue.Queue() + self.lifecycle_lock = threading.RLock() + self.status_lock = threading.Lock() + self.shutdown_watcher = None + self.shutdown_read_fd = None + self.shutdown_write_fd = None + self.instance_lock = None + self.stopping = False + self.finalizing_recording = False + self.startup_complete = False + cleanup_model = ( + cfg.cleanup_model or backends.CLEANUP[cfg.cleanup_engine].default_model + ) + self.status_details = { + "stt": backends.short_model(self.stt_model), + "cleanup": backends.short_model(cleanup_model) + if cfg.cleanup_enabled + else "off", + "mode": cfg.mode, + } + + # --- live status for CLI and menu-bar clients -------------------------- + # Written on every transition (and the icon repainted) so actions get + # immediate feedback, including loading, active work, errors, and shutdown. + def _set_state(self, state: str, error: str = "") -> None: + with self.status_lock: + if self.stopping and state != "stopping": + return + clean_error = " ".join(error.split()) + config.write_status( + { + "pid": os.getpid(), + "state": state, + **self.status_details, + "error": clean_error, + } + ) + + def _clear_status(self) -> None: + with self.status_lock: + config.clear_status() + + def _start_shutdown_watcher(self) -> None: + self.shutdown_read_fd, self.shutdown_write_fd = os.pipe() + + def watch(): + os.read(self.shutdown_read_fd, 1) + self._set_state("stopping") + + self.shutdown_watcher = threading.Thread( + target=watch, name="v2t-shutdown-status", daemon=True + ) + self.shutdown_watcher.start() + + def _close_stream(self) -> None: + if self.stream is not None: + stream, self.stream = self.stream, None + try: + stream.stop() + except Exception as error: + logger.warning(f"Could not stop audio input cleanly: {error}") + try: + stream.close() + except Exception as error: + logger.warning(f"Could not close audio input cleanly: {error}") + + def _restore_media(self) -> None: + if self.cfg.pause_music and self.was_playing: + subprocess.run(["nowplaying-cli", "play"], check=False) + self.was_playing = False + + # --- recording ---------------------------------------------------------- + def audio_callback(self, indata, frame_count, time_info, status): + if status: + logger.warning(f"Audio input: {status}") + if self.recording: + self.frames.append(indata.copy()) + + def start_recording(self): + with self.lifecycle_lock: + if self.stopping or self.recording or self.processing: + return + self.frames = [] + self.record_start = time.perf_counter() + try: + self.stream = sd.InputStream( + samplerate=self.cfg.sample_rate, + channels=1, + dtype="float32", + callback=self.audio_callback, + ) + self.recording = True + self.stream.start() + except Exception as error: + self.recording = False + self._close_stream() + logger.error(f"Could not open the microphone: {error}") + self._set_state("error", f"Microphone unavailable: {error}") + if not self._warned_mic: + self._warned_mic = True + subprocess.run(["open", MIC_PANE], check=False) + return + if self.stopping: + self.recording = False + self._close_stream() + return + + if self.cfg.pause_music: + result = subprocess.run( + ["nowplaying-cli", "get", "playbackRate"], + capture_output=True, + text=True, + check=False, + ) + self.was_playing = result.stdout.strip() == "1" + if self.was_playing: + subprocess.run(["nowplaying-cli", "pause"], check=False) + self._set_state("recording") + logger.info("Recording...") + + def stop_recording(self): + with self.lifecycle_lock: + if not self.recording: + return + self.recording = False + self.finalizing_recording = True + try: + duration = time.perf_counter() - self.record_start + self._close_stream() + logger.info(f"Stopped ({duration:.1f}s)") + if self.frames: + frames, self.frames = self.frames, [] + self.processing = True + self.jobs.put((frames, duration)) + else: + self._restore_media() + self._set_state("idle") + finally: + self.finalizing_recording = False + + def process_audio(self, frames: list[np.ndarray], audio_s: float): + next_state, error_message, temp_path = "idle", "", None + try: + self._set_state("transcribing") + audio = np.concatenate(frames, axis=0) + if ( + float(np.abs(audio).max()) < 1e-4 + ): # dead silence == no mic access, not a quiet room + error_message = "No audio captured. Check Microphone permission, then restart the launching app." + logger.error(error_message) + if not self._warned_mic: + self._warned_mic = True + subprocess.run(["open", MIC_PANE], check=False) + next_state = "error" + return + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: + wavfile.write( + f.name, self.cfg.sample_rate, (audio * 32767).astype(np.int16) + ) + temp_path = f.name + + logger.info("Transcribing...") + t0 = time.perf_counter() + raw_text = self.stt.transcribe(temp_path) + stt_s = time.perf_counter() - t0 + logger.info(f"Transcribed {len(raw_text)} characters ({stt_s:.2f}s)") + if not raw_text: + logger.warning("No speech detected") + return + + cleaned_text, cleanup_s = raw_text, 0.0 + if self.cleaner is not None: + self._set_state("cleaning") + logger.info("Cleaning up...") + try: + cleaned_text, _ttft, cleanup_s = self.cleaner.cleanup( + raw_text, self.cfg.mode + ) + if not cleaned_text: + raise RuntimeError("empty response") + logger.info( + f"Cleaned {len(cleaned_text)} characters ({cleanup_s:.2f}s)" + ) + except Exception as e: + logger.error(f"LLM cleanup failed: {e}") + logger.warning("Falling back to raw transcription") + cleaned_text = raw_text + + t0 = time.perf_counter() + self.paste_to_cursor(cleaned_text) + paste_s = time.perf_counter() - t0 + logger.success(f"Pasted ({paste_s:.2f}s including clipboard restore)") + + if self.cfg.save_history: + try: + config.append_history( + { + "audio_s": round(audio_s, 2), + "backend": self.cfg.backend, + "model": self.stt_model, + "cleanup_engine": self.cfg.cleanup_engine + if self.cleaner + else None, + "cleanup_model": self.cleaner.model_id + if self.cleaner + else None, + "mode": self.cfg.mode, + "stt_s": round(stt_s, 3), + "cleanup_s": round(cleanup_s, 3), + "paste_s": round(paste_s, 3), + "raw": raw_text, + "clean": cleaned_text, + } + ) + except OSError as error: + logger.warning(f"Could not save transcription history: {error}") + except Exception as error: + next_state, error_message = "error", f"{type(error).__name__}: {error}" + logger.exception(f"Transcription failed: {error}") + finally: + if temp_path: + Path(temp_path).unlink(missing_ok=True) + self._restore_media() + if not self.stopping: + self._set_state(next_state, error_message) + self.processing = False + + def process_next(self, timeout: float | None = None) -> bool: + """Process one queued recording on the model-owning thread.""" + try: + job = self.jobs.get(timeout=timeout) + except queue.Empty: + return True + if job is None: + return False + self.process_audio(*job) + return True + + def _keep_running(self) -> bool: + with self.lifecycle_lock: + return ( + not self.stopping + or self.finalizing_recording + or self.processing + ) + + def paste_to_cursor(self, text: str) -> None: + """Paste at the cursor, preserving every native pasteboard representation.""" + from AppKit import NSPasteboard, NSPasteboardItem, NSPasteboardTypeString + from Quartz import ( + CGEventCreateKeyboardEvent, + CGEventPost, + CGEventSetFlags, + kCGEventFlagMaskCommand, + kCGHIDEventTap, + ) + + pasteboard = NSPasteboard.generalPasteboard() + saved = None + for _ in range(2): + snapshot_change = pasteboard.changeCount() + snapshot = [] + for item in pasteboard.pasteboardItems() or []: + values = [] + for kind in item.types(): + data = item.dataForType_(kind) + if data is not None: + values.append((kind, data)) + snapshot.append(values) + if pasteboard.changeCount() == snapshot_change: + saved = snapshot + break + if saved is None: + raise RuntimeError("clipboard changed while preparing paste") + dictated_change = None + try: + pasteboard.clearContents() + if not pasteboard.setString_forType_(text, NSPasteboardTypeString): + raise RuntimeError("could not write to the clipboard") + dictated_change = pasteboard.changeCount() + down = CGEventCreateKeyboardEvent(None, 9, True) + up = CGEventCreateKeyboardEvent(None, 9, False) + CGEventSetFlags(down, kCGEventFlagMaskCommand) + CGEventSetFlags(up, kCGEventFlagMaskCommand) + CGEventPost(kCGHIDEventTap, down) + CGEventPost(kCGHIDEventTap, up) + # Give slower targets time to consume the synthetic paste before restoration. + time.sleep(0.3) + finally: + should_restore = ( + dictated_change is None + or pasteboard.changeCount() == dictated_change + ) + if should_restore: + pasteboard.clearContents() + restored = [] + for values in saved: + item = NSPasteboardItem.alloc().init() + for kind, data in values: + item.setData_forType_(data, kind) + restored.append(item) + if restored: + pasteboard.writeObjects_(restored) + + # --- run loop ----------------------------------------------------------- + def on_press(self, key): + if key == self.hotkey: + self.start_recording() + + def on_release(self, key): + if key == self.hotkey: + self.stop_recording() + + def warmup(self): + logger.info("Loading transcription model...") + self._set_state("loading-stt") + t0 = time.perf_counter() + self.stt = backends.make_stt(self.cfg.backend, self.cfg.stt_model) + temp_path = None + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: + wavfile.write( + f.name, + self.cfg.sample_rate, + np.zeros(self.cfg.sample_rate, dtype=np.int16), + ) + temp_path = f.name + try: + self.stt.transcribe(temp_path) + finally: + Path(temp_path).unlink(missing_ok=True) + logger.success(f"{self.cfg.backend} ready ({time.perf_counter() - t0:.1f}s)") + + if self.cfg.cleanup_enabled: + self._set_state("loading-cleanup") + t0 = time.perf_counter() + self.cleaner = backends.make_cleanup( + self.cfg.cleanup_engine, self.cfg.cleanup_model, self.cfg.ollama_url + ) + try: + self.cleaner.cleanup("hi", self.cfg.mode) + logger.success( + f"cleanup ({self.cfg.cleanup_engine}) ready ({time.perf_counter() - t0:.1f}s)" + ) + except Exception as e: + logger.warning( + f"cleanup warmup failed ({e}); will retry per transcription" + ) + + def run(self): + from pynput import keyboard + + try: + self.instance_lock = config.acquire_instance_lock() + except BlockingIOError: + logger.error("v2t already running. Stop it first: v2t stop") + sys.exit(1) + config.clear_last_error() + config.ensure_dirs() + self.hotkey = _resolve_hotkey(self.cfg.hotkey) + self._start_shutdown_watcher() + signal.signal(signal.SIGTERM, self._handle_signal) + signal.signal(signal.SIGINT, self._handle_signal) + try: + self.warmup() + self.startup_complete = True + self._set_state("idle") + logger.info(f"Voice-to-Text — {self.cfg.backend} · {self.cfg.mode}") + if self.cfg.pause_music: + logger.info("Pause Music — on") + logger.info( + f"Hold {self.cfg.hotkey} to record, release to transcribe and paste. Ctrl+C to quit." + ) + with keyboard.Listener( + on_press=self.on_press, on_release=self.on_release + ) as listener: + while listener.is_alive() and self._keep_running(): + if not self.process_next(timeout=0.25): + break + if not self.stopping and not listener.is_alive(): + raise RuntimeError("global hotkey listener stopped unexpectedly") + except KeyboardInterrupt: + logger.info("Shutting down...") + except SystemExit as error: + if error.code not in (None, 0): + config.write_last_error(f"v2t stopped with an error: {error}") + raise + except Exception as error: + config.write_last_error(f"v2t stopped with an error: {error}") + raise + finally: + self.shutdown() + + def _handle_signal(self, signum=None, _frame=None): + if self.stopping: + if signum == signal.SIGINT: + raise SystemExit(0) + return + self.stopping = True + if self.shutdown_write_fd is not None: + try: + os.write(self.shutdown_write_fd, b"\0") + except OSError: + pass + if ( + not self.startup_complete + and not self.finalizing_recording + and not self.processing + ): + raise SystemExit(0) + + def shutdown(self) -> None: + self.stopping = True + if self.shutdown_write_fd is not None: + try: + os.write(self.shutdown_write_fd, b"\0") + except OSError: + pass + if self.shutdown_watcher is not None: + self.shutdown_watcher.join() + self._set_state("stopping") + self.jobs.put(None) + with self.lifecycle_lock: + self.recording = False + self._close_stream() + self._restore_media() + self._clear_status() + for fd in (self.shutdown_read_fd, self.shutdown_write_fd): + if fd is not None: + os.close(fd) + self.shutdown_read_fd = self.shutdown_write_fd = None + if self.instance_lock is not None: + self.instance_lock.close() + self.instance_lock = None + + +if __name__ == "__main__": + # Logic here needs audio + MLX; pure helpers/tests live in backends.py & config.py. + print("app.py: import OK") diff --git a/v2t/backends.py b/v2t/backends.py new file mode 100644 index 0000000..9cb8e0c --- /dev/null +++ b/v2t/backends.py @@ -0,0 +1,203 @@ +"""Speech-to-text backends and text-cleanup engines, both pluggable. + +STT: parakeet (default) or whisper, both MLX. Cleanup: mlx (in-process via +mlx-lm, default — no daemon) or ollama. Heavy MLX imports are lazy so +`v2t config`/`v2t bench` work without a model loaded. +""" + +from __future__ import annotations + +import json +import time +import urllib.request + +PARAKEET_DEFAULT = "mlx-community/parakeet-tdt-0.6b-v3" +WHISPER_DEFAULT = "mlx-community/whisper-large-v3-turbo" + + +class ParakeetSTT: + default_model = PARAKEET_DEFAULT + + def __init__(self, model: str = ""): + try: + from parakeet_mlx import from_pretrained + except ImportError as e: + raise SystemExit( + "parakeet-mlx missing — reinstall voice2text (Apple Silicon only)." + ) from e + self.model = from_pretrained(model or self.default_model) + + def transcribe(self, wav_path: str) -> str: + return self.model.transcribe(wav_path).text.strip() + + +class WhisperSTT: + default_model = WHISPER_DEFAULT + + def __init__(self, model: str = ""): + try: + import mlx_whisper + except ImportError as e: + raise SystemExit( + "whisper backend needs: uv tool install 'voice2text[whisper]'" + ) from e + self._mlx_whisper = mlx_whisper + self.model = model or self.default_model + + def transcribe(self, wav_path: str) -> str: + return self._mlx_whisper.transcribe(wav_path, path_or_hf_repo=self.model)[ + "text" + ].strip() + + +STT = {"parakeet": ParakeetSTT, "whisper": WhisperSTT} + + +def make_stt(backend: str, model: str = ""): + if backend not in STT: + raise SystemExit(f"unknown backend {backend!r}; choose: {', '.join(STT)}") + return STT[backend](model) + + +PROMPTS = { + "strict": ( + "Clean up this transcription. Fix punctuation, remove filler words " + "(um, uh, like, you know), fix obvious mishearings, keep the meaning intact. " + "Output ONLY the cleaned text, nothing else:\n\n{text}" + ), + "casual": ( + "Lightly clean up this transcription. Only fix punctuation and remove filler " + "words (um, uh, like, you know). Do NOT restructure sentences or change word " + "order. Keep the original phrasing. Output ONLY the cleaned text, nothing " + "else:\n\n{text}" + ), +} + +MLX_CLEANUP_DEFAULT = "mlx-community/Qwen2.5-1.5B-Instruct-4bit" +OLLAMA_CLEANUP_DEFAULT = "qwen3:4b-instruct-2507" + + +class MLXCleanup: + """In-process cleanup via mlx-lm — no daemon, no HTTP. The default. Pick a + non-thinking instruct model (the default Qwen2.5-Instruct doesn't think).""" + + default_model = MLX_CLEANUP_DEFAULT + + def __init__(self, model: str = "", url: str = ""): + try: + from mlx_lm import load, stream_generate + except ImportError as e: + raise SystemExit( + "mlx-lm missing — reinstall voice2text (Apple Silicon only)." + ) from e + self._stream = stream_generate + self.model_id = model or self.default_model + self.model, self.tokenizer = load(self.model_id) + + def cleanup(self, text: str, mode: str = "strict"): + messages = [{"role": "user", "content": PROMPTS[mode].format(text=text)}] + prompt = self.tokenizer.apply_chat_template( + messages, add_generation_prompt=True + ) + max_tokens = min(max(400, len(self.tokenizer.encode(text)) + 128), 4096) + t0, ttft, parts = time.perf_counter(), None, [] + for resp in self._stream( + self.model, self.tokenizer, prompt, max_tokens=max_tokens + ): + if ttft is None: + ttft = time.perf_counter() - t0 + parts.append(resp.text) + if len(parts) >= max_tokens: + raise RuntimeError( + "cleanup hit its token limit; using the raw transcription" + ) + return "".join(parts).strip(), ttft, time.perf_counter() - t0 + + +class OllamaCleanup: + """Cleanup via a running Ollama server — for people who already use it.""" + + default_model = OLLAMA_CLEANUP_DEFAULT + + def __init__(self, model: str = "", url: str = "http://localhost:11434"): + self.model_id = model or self.default_model + self.url = url + + def cleanup(self, text: str, mode: str = "strict", timeout: int = 60): + prompt = PROMPTS[mode].format(text=text) + req = urllib.request.Request( + f"{self.url}/api/generate", + data=json.dumps( + {"model": self.model_id, "prompt": prompt, "stream": True} + ).encode(), + headers={"Content-Type": "application/json"}, + ) + t0, ttft, parts = time.perf_counter(), None, [] + with urllib.request.urlopen( + req, timeout=timeout + ) as r: # needs a running ollama server + for line in r: + if not line.strip(): + continue + chunk = json.loads(line) + if piece := chunk.get("response", ""): + if ttft is None: + ttft = time.perf_counter() - t0 + parts.append(piece) + if chunk.get("error"): + raise RuntimeError(chunk["error"]) + if chunk.get("done"): + break + return "".join(parts).strip(), ttft, time.perf_counter() - t0 + + +CLEANUP = {"mlx": MLXCleanup, "ollama": OllamaCleanup} + + +def make_cleanup(engine: str, model: str = "", url: str = "http://localhost:11434"): + if engine not in CLEANUP: + raise SystemExit( + f"unknown cleanup engine {engine!r}; choose: {', '.join(CLEANUP)}" + ) + return CLEANUP[engine](model, url) + + +_LABELS = { + "parakeet-tdt-0.6b-v3": "parakeet-v3", + "parakeet-tdt-0.6b-v2": "parakeet-v2", + "whisper-large-v3-turbo": "whisper-turbo", + "Qwen2.5-1.5B-Instruct-4bit": "Qwen2.5-1.5B", + "Qwen3-4B-Instruct-2507-4bit": "Qwen3-4B", + "Qwen2.5-3B-Instruct-4bit": "Qwen2.5-3B", + "qwen3:4b-instruct-2507": "qwen3:4b", +} + + +def short_model(name: str) -> str: + """Friendly menu-bar label, e.g. mlx-community/parakeet-tdt-0.6b-v3 -> parakeet-v3.""" + tail = name.rsplit("/", 1)[-1] + return _LABELS.get(tail, tail) + + +if __name__ == "__main__": + # ponytail: pure-logic checks only; live model calls are covered by `v2t bench`. + assert set(STT) == {"parakeet", "whisper"} + assert set(CLEANUP) == {"mlx", "ollama"} + assert short_model("mlx-community/parakeet-tdt-0.6b-v3") == "parakeet-v3" + assert ( + short_model("mlx-community/Qwen2.5-1.5B-Instruct-4bit") + == "Qwen2.5-1.5B" + ) + assert short_model("custom/unknown") == "unknown" + assert ( + "filler" in PROMPTS["strict"] + and "Keep the original phrasing" in PROMPTS["casual"] + ) + for fn in (make_stt, make_cleanup): + try: + fn("bogus") + except SystemExit: + pass + else: + raise AssertionError(f"{fn.__name__} must exit on unknown name") + print("backends.py: all checks passed") diff --git a/v2t/bench.py b/v2t/bench.py new file mode 100644 index 0000000..6d6c3d1 --- /dev/null +++ b/v2t/bench.py @@ -0,0 +1,307 @@ +"""Benchmark STT and cleanup models -> a markdown grid. + +Each table is models-as-columns. STT and the default mlx cleanup need Apple Silicon; +the ollama cleanup engine needs Ollama. Results are written under +~/.v2t/benchmarks/results/ so you can collect a grid across machines. Absolute +times reflect THAT machine — compare models within a run, not across machines. + + just bench # both tables, default models, samples via `say` + just bench --cleanup # cleanup table only + just bench --cleanup-models mlx:mlx-community/Qwen2.5-1.5B-Instruct-4bit +""" + +from __future__ import annotations + +import argparse +import platform +import statistics +import subprocess +import sys +import time +import wave +from datetime import date +from pathlib import Path + +from . import backends, config + +STT_MODELS = [ + "parakeet:mlx-community/parakeet-tdt-0.6b-v3", + "parakeet:mlx-community/parakeet-tdt-0.6b-v2", + "whisper:mlx-community/whisper-large-v3-turbo", +] +CLEANUP_MODELS = [ + "mlx:mlx-community/Qwen2.5-1.5B-Instruct-4bit", + "mlx:mlx-community/Qwen3-4B-Instruct-2507-4bit", +] + +# (name, text) — `say` turns these into audio so the STT bench is self-contained. +SAY_SAMPLES = [ + ("short", "Hey, can you send me the report by end of day? Thanks a lot."), + ( + "medium", + "So basically I was thinking we could try the other approach for the migration, " + "move the read traffic over first, watch the error rates for a day, and then cut the " + "writes once we're confident nothing is on fire.", + ), + ( + "long", + "Right, so the plan for next quarter. First, we finish the local transcription work, " + "because the cloud dependency keeps breaking behind the corporate VPN and it's just not " + "worth fighting. Second, we ship the menu bar toggle so it's one click to start and stop. " + "Third, we benchmark every model on each machine, write the numbers down, and stop guessing " + "which one is fastest. None of this is hard, it just needs doing, and I'd rather spend the " + "time once than keep re-litigating it every single week in standup.", + ), +] + +# Filler-laden raw transcriptions for the cleanup table. +CLEANUP_SAMPLES = [ + "Hey um I'll see you tomorrow at 9 actually no make it 10", + "So basically I was thinking we could um you know maybe try the other approach", + "yeah so the the thing is like we need to ship this by friday otherwise uh it slips again", +] + + +def host_label() -> str: + if platform.system() == "Darwin": + + def sysctl(key): + return subprocess.run( + ["sysctl", "-n", key], capture_output=True, text=True + ).stdout.strip() + + chip = sysctl("machdep.cpu.brand_string") or sysctl("hw.model") or "mac" + return chip.replace(" ", "-") + return platform.machine() + "-" + platform.system().lower() + + +def wav_duration(path: Path) -> float: + with wave.open(str(path)) as w: + return w.getnframes() / w.getframerate() + + +def ensure_samples(directory: Path) -> list[tuple[str, Path, float]]: + """Synthesize the SAY_SAMPLES with macOS `say` -> 16k mono wav (via afconvert).""" + directory.mkdir(parents=True, exist_ok=True) + out = [] + for name, text in SAY_SAMPLES: + wav = directory / f"{name}.wav" + if not wav.exists(): + aiff = directory / f"{name}.aiff" + subprocess.run(["say", "-o", str(aiff), text], check=True) + subprocess.run( + [ + "afconvert", + "-f", + "WAVE", + "-d", + "LEI16@16000", + "-c", + "1", + str(aiff), + str(wav), + ], + check=True, + ) + aiff.unlink(missing_ok=True) + out.append((name, wav, wav_duration(wav))) + return out + + +def _median(xs: list[float]) -> float: + return statistics.median(xs) + + +def bench_stt(model_specs: list[str], samples, repeat: int) -> dict: + """{spec: {"load": s, sample_name: (proc_s, rtf)} | None}.""" + results = {} + for spec in model_specs: + backend, _, model = spec.partition(":") + print(f" STT {spec} ... loading") + t0 = time.perf_counter() + try: + stt = backends.make_stt(backend, model) + except (Exception, SystemExit) as error: + print(f" skipped ({error})") + results[spec] = None + continue + results[spec] = {"load": time.perf_counter() - t0} + for name, wav, dur in samples: + stt.transcribe( + str(wav) + ) # warm up: MLX compiles per input shape; the app warms up too + times = [] + for _ in range(repeat): + t = time.perf_counter() + stt.transcribe(str(wav)) + times.append(time.perf_counter() - t) + proc = _median(times) + results[spec][name] = (proc, dur / proc if proc else 0.0) + print(f" {name} ({dur:.1f}s): {proc:.2f}s RTF {dur / proc:.0f}x") + return results + + +def bench_cleanup(specs: list[str], samples: list[str], repeat: int, url: str) -> dict: + """{spec: {sample_idx: (ttft_s, total_s)} | None}. spec is 'engine:model'.""" + results = {} + for spec in specs: + engine, _, model = spec.partition(":") + print(f" cleanup {spec} ...") + try: + cleaner = backends.make_cleanup(engine, model, url) + cleaner.cleanup("hi") # warm / pull + except (Exception, SystemExit) as e: # one missing engine shouldn't abort the run + print(f" skipped ({e})") + results[spec] = None + continue + results[spec] = {} + for i, sample in enumerate(samples): + ttfts, totals = [], [] + for _ in range(repeat): + _text, ttft, total = cleaner.cleanup(sample) + ttfts.append(ttft if ttft is not None else total) + totals.append(total) + results[spec][i] = (_median(ttfts), _median(totals)) + print( + f" sample {i}: ttft {results[spec][i][0]:.2f}s total {results[spec][i][1]:.2f}s" + ) + return results + + +def md_stt_table(results: dict, samples) -> str: + specs = list(results) + + def label(s: str) -> str: + backend, _, model = s.partition(":") + return f"{backend} {model.rsplit('/', 1)[-1]}" + + head = "| sample | " + " | ".join(label(s) for s in specs) + " |" + sep = "|---|" + "|".join("--:" for _ in specs) + "|" + rows = [ + "| load | " + + " | ".join( + "n/a" if results[s] is None else f"{results[s]['load']:.1f}s" for s in specs + ) + + " |" + ] + for name, _wav, dur in samples: + cells = " | ".join( + "n/a" + if results[s] is None + else f"{results[s][name][0]:.2f}s ({results[s][name][1]:.0f}x)" + for s in specs + ) + rows.append(f"| {name} ({dur:.1f}s) | {cells} |") + note = "\n_Cells: transcription time (real-time factor). Higher RTF = faster._" + return ( + "\n".join(["**Speech-to-text** — time & RTF", "", head, sep, *rows]) + + "\n" + + note + ) + + +def md_cleanup_table(results: dict, samples: list[str]) -> str: + specs = list(results) + + def label(s: str) -> str: + engine, _, model = s.partition(":") + return f"{engine}:{model.rsplit('/', 1)[-1]}" + + def cell(spec: str, i: int) -> str: + r = results[spec] + return "n/a" if r is None else f"{r[i][0]:.2f} / {r[i][1]:.2f}s" + + head = "| sample | " + " | ".join(label(s) for s in specs) + " |" + sep = "|---|" + "|".join("--:" for _ in specs) + "|" + rows = [ + f"| {i} | " + " | ".join(cell(s, i) for s in specs) + " |" + for i in range(len(samples)) + ] + med = [] + for s in specs: + r = results[s] + med.append( + "n/a" + if r is None + else f"{_median([r[i][0] for i in range(len(samples))]):.2f} / " + f"{_median([r[i][1] for i in range(len(samples))]):.2f}s" + ) + rows.append("| **median** | " + " | ".join(med) + " |") + note = "\n_Cells: time-to-first-token / total. Lower is better._" + return ( + "\n".join(["**Text cleanup** — TTFT & total", "", head, sep, *rows]) + + "\n" + + note + ) + + +def write_results(body: str, out: Path | None) -> Path: + out = out or ( + config.home() / "benchmarks/results" / f"{date.today()}-{host_label()}.md" + ) + out.parent.mkdir(parents=True, exist_ok=True) + header = f"# v2t benchmark — {host_label()} — {date.today()}\n\n_Compare models within this file; absolute times are machine-specific._\n\n" + out.write_text(header + body + "\n") + return out + + +def main(argv: list[str]) -> int: + def positive_int(value: str) -> int: + parsed = int(value) + if parsed < 1: + raise argparse.ArgumentTypeError("must be at least 1") + return parsed + + config.ensure_dirs() + p = argparse.ArgumentParser(prog="v2t bench") + p.add_argument("--stt", action="store_true", help="run the STT table only") + p.add_argument("--cleanup", action="store_true", help="run the cleanup table only") + p.add_argument( + "--stt-models", nargs="+", default=STT_MODELS, help="backend:model specs" + ) + p.add_argument( + "--cleanup-models", + nargs="+", + default=CLEANUP_MODELS, + help="engine:model specs (mlx:… or ollama:…)", + ) + p.add_argument( + "--repeat", type=positive_int, default=3, help="runs per cell, median reported" + ) + p.add_argument( + "--audio", + type=Path, + default=config.home() / "bench-audio", + help="sample wav dir", + ) + p.add_argument("--url", default="http://localhost:11434", help="ollama base url") + p.add_argument("--out", type=Path, help="results markdown path") + a = p.parse_args(argv) + + do_stt = a.stt or not a.cleanup + do_cleanup = a.cleanup or not a.stt + sections = [] + + if do_stt: + print("Speech-to-text:") + samples = ensure_samples(a.audio) + sections.append( + md_stt_table(bench_stt(a.stt_models, samples, a.repeat), samples) + ) + if do_cleanup: + print("Text cleanup:") + sections.append( + md_cleanup_table( + bench_cleanup(a.cleanup_models, CLEANUP_SAMPLES, a.repeat, a.url), + CLEANUP_SAMPLES, + ) + ) + + out = write_results("\n\n".join(sections), a.out) + print(f"\nwrote {out}") + print("\n" + "\n\n".join(sections)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/v2t/cli.py b/v2t/cli.py new file mode 100644 index 0000000..44b8120 --- /dev/null +++ b/v2t/cli.py @@ -0,0 +1,311 @@ +"""v2t command line. + +v2t run push-to-talk (default) +v2t setup guided first-run config (pick models, detect Ollama) + v2t config show resolved config + paths (--init to write a template) + v2t status live state line (off / starting / idle / recording / …) + v2t stop stop a running v2t + v2t menubar install/open the optional native menu-bar app + v2t service start the menu app automatically at login +""" + +from __future__ import annotations + +import argparse +import os +import signal +import sys + +from . import config + + +def cmd_run(argv: list[str]) -> int: + p = argparse.ArgumentParser(prog="v2t", description="push-to-talk voice-to-text") + p.add_argument("--config", help="path to a config.toml") + p.add_argument( + "--backend", + choices=["parakeet", "whisper"], + help="override transcription backend", + ) + p.add_argument("--model", help="override STT model") + p.add_argument( + "--cleanup-engine", choices=["mlx", "ollama"], help="override cleanup engine" + ) + p.add_argument("--cleanup-model", help="override cleanup model") + p.add_argument( + "--no-cleanup", + action="store_true", + help="paste raw transcription, skip LLM cleanup", + ) + cleanup_mode = p.add_mutually_exclusive_group() + cleanup_mode.add_argument( + "--casual", + action="store_true", + help="light cleanup (punctuation + fillers only)", + ) + cleanup_mode.add_argument( + "--strict", + action="store_true", + help="full cleanup (restructures) — the default", + ) + p.add_argument( + "--pause-music", + action="store_true", + help="pause media while recording (needs nowplaying-cli)", + ) + a = p.parse_args(argv) + + if a.config: + os.environ["V2T_CONFIG"] = a.config + overrides = { + "backend": a.backend, + "stt_model": a.model, + "cleanup_engine": a.cleanup_engine, + "cleanup_model": a.cleanup_model, + "cleanup_enabled": False if a.no_cleanup else None, + "mode": "casual" if a.casual else ("strict" if a.strict else None), + "pause_music": True if a.pause_music else None, + } + cfg = config.load(overrides) + + from . import app # lazy: needs audio + MLX, unlike the commands above + + if cfg.pause_music and not _which("nowplaying-cli"): + from loguru import logger + + logger.warning( + "nowplaying-cli not found (brew install nowplaying-cli) — music pause disabled." + ) + cfg.pause_music = False + if os.environ.get("V2T_LAUNCH_CONTEXT") != "menubar": + app.check_and_request_permissions() + app.VoiceToText(cfg).run() + return 0 + + +def cmd_config(argv: list[str]) -> int: + p = argparse.ArgumentParser(prog="v2t config") + p.add_argument( + "--init", action="store_true", help="write a commented config.toml if absent" + ) + p.add_argument("--path", action="store_true", help="print paths only") + a = p.parse_args(argv) + + if a.init: + print(f"wrote {config.write_default()}") + return 0 + print(f"home: {config.home()}") + print( + f"config: {config.config_path()}{'' if config.config_path().exists() else ' (using defaults; v2t config --init to create)'}" + ) + print(f"history: {config.history_path()}") + if not a.path: + from dataclasses import asdict + + print("\n[effective config]") + for k, v in asdict(config.load()).items(): + print(f" {k} = {v!r}") + return 0 + + +_SETUP_TOML = """\ +# Written by `v2t setup`. Run `v2t config` to see every option, or edit freely. + +[transcription] +backend = "{backend}" +model = "" + +[cleanup] +enabled = {enabled} +engine = "{engine}" +model = "" +mode = "strict" +""" + + +def _ask(prompt: str, options: list[tuple[str, str]], default: int = 0) -> str: + """Numbered single-choice prompt. options = [(value, label)]; returns the value.""" + print(prompt) + for i, (_v, label) in enumerate(options): + print(f" {i + 1}) {label}{' (default)' if i == default else ''}") + raw = input(f" choice [{default + 1}]: ").strip() + idx = int(raw) - 1 if raw.isdigit() and 1 <= int(raw) <= len(options) else default + return options[idx][0] + + +def _yesno(prompt: str, default: bool = True) -> bool: + raw = input(f"{prompt} [{'Y/n' if default else 'y/N'}]: ").strip().lower() + return default if not raw else raw.startswith("y") + + +def cmd_setup(argv: list[str]) -> int: + from shutil import which + + path = config.config_path() + print(f"v2t setup → {path}\n") + if path.exists() and not _yesno(f"{path} exists. Overwrite?", default=False): + print("keeping existing config.") + return 0 + + backend = _ask( + "Transcription model:", + [ + ("parakeet", "Parakeet — fast, multilingual (recommended)"), + ("whisper", "Whisper — best for rare languages / accents"), + ], + ) + engine = "mlx" + cleanup_enabled = _yesno( + "\nAdd an LLM cleanup pass (punctuation, fillers)?", default=True + ) + if cleanup_enabled: + if which("ollama"): + engine = _ask( + "\nOllama detected. Cleanup engine:", + [ + ("mlx", "mlx-lm — in-process, no daemon (recommended)"), + ("ollama", "Ollama — reuse what you already run"), + ], + ) + else: + print("\nNo Ollama found — using in-process mlx-lm for cleanup.") + + config.write_config( + _SETUP_TOML.format( + backend=backend, enabled=str(cleanup_enabled).lower(), engine=engine + ), + path, + ) + print(f"\nwrote {path}") + if cleanup_enabled and engine == "ollama": + print("next: ollama pull qwen3:4b-instruct-2507") + package = "'voice2text[whisper]'" if backend == "whisper" else "voice2text" + print(f"run: v2t (install with: uv tool install {package})") + return 0 + + +def cmd_status(argv: list[str]) -> int: + """Runtime state, models, mode, and any launch error.""" + from . import backends + + s = config.read_status() + if s: + state, stt, cleanup, mode, error = ( + s["state"], + s["stt"], + s["cleanup"], + s["mode"], + s.get("error", ""), + ) + else: + cfg = config.load() + error = config.read_last_error() + state = "launch-error" if error else "off" + stt = backends.short_model( + cfg.stt_model or backends.STT[cfg.backend].default_model + ) + cleanup = ( + backends.short_model( + cfg.cleanup_model or backends.CLEANUP[cfg.cleanup_engine].default_model + ) + if cfg.cleanup_enabled + else "off" + ) + mode = cfg.mode + fields = [ + " ".join(str(value).replace("|", "/").split()) + for value in ( + state, + stt, + cleanup, + mode, + error, + ) + ] + print("\t".join(fields)) + return 0 + + +def cmd_stop(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog="v2t stop") + parser.add_argument( + "--force", action="store_true", help="stop immediately instead of waiting" + ) + args = parser.parse_args(argv) + pid = config.running_pid() + if pid is None: + config.clear_status() + print("not running") + return 1 + os.kill(pid, signal.SIGKILL if args.force else signal.SIGTERM) + action = "force-stopped" if args.force else "stopping" + print(f"{action} v2t (pid {pid})") + return 0 + + +def cmd_menubar(argv: list[str]) -> int: + from . import menubar + + parser = argparse.ArgumentParser( + prog="v2t menubar", description="install or open the optional menu-bar app" + ) + parser.add_argument( + "action", nargs="?", choices=["install", "open"], default="install" + ) + args = parser.parse_args(argv) + if args.action == "install": + path = menubar.install() + print(f"installed {path}") + menubar.open_app() + else: + menubar.open_app() + return 0 + + +def cmd_service(argv: list[str]) -> int: + from . import service + + parser = argparse.ArgumentParser(prog="v2t service") + parser.add_argument( + "action", choices=["install", "start", "stop", "status", "uninstall"] + ) + args = parser.parse_args(argv) + if args.action == "status": + print(service.status()) + return 0 + try: + if args.action == "install": + path = service.install() + print(f"installed for login {path}") + print(f"menu app: {service.menubar.app_path()}") + return 0 + getattr(service, args.action)() + except RuntimeError as error: + raise SystemExit(f"service {args.action} failed: {error}") from error + print(f"service {args.action} complete") + return 0 + + +def _which(name: str) -> bool: + from shutil import which + + return which(name) is not None + + +def main(argv: list[str] | None = None) -> int: + argv = list(sys.argv[1:] if argv is None else argv) + table = { + "setup": cmd_setup, + "config": cmd_config, + "status": cmd_status, + "stop": cmd_stop, + "service": cmd_service, + "menubar": cmd_menubar, + } + if argv and argv[0] in table: + return table[argv[0]](argv[1:]) + return cmd_run(argv[1:] if argv and argv[0] == "run" else argv) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/v2t/config.py b/v2t/config.py new file mode 100644 index 0000000..e06836c --- /dev/null +++ b/v2t/config.py @@ -0,0 +1,329 @@ +"""Config + paths for v2t. + +Everything lives under ~/.v2t (or $V2T_HOME, or $XDG_CONFIG_HOME/v2t): + config.toml user settings + history/transcriptions.jsonl every transcription + metadata + run/status.json live state for CLI and menu-bar clients + +Zero config works: the defaults below are the shipped behaviour +(Parakeet + Qwen2.5, MLX, strict cleanup). +""" + +from __future__ import annotations + +import fcntl +import json +import os +import tempfile +import tomllib +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path + + +@dataclass +class Config: + backend: str = "parakeet" # parakeet | whisper + stt_model: str = "" # blank = the backend's own default + cleanup_enabled: bool = True + cleanup_engine: str = "mlx" # mlx (in-process via mlx-lm) | ollama + cleanup_model: str = "" # blank = the engine's own default + mode: str = "strict" # strict | casual + hotkey: str = "cmd_r" + sample_rate: int = 16000 + pause_music: bool = False + save_history: bool = True + ollama_url: str = "http://localhost:11434" + + +def home() -> Path: + """The v2t home directory. $V2T_HOME > $XDG_CONFIG_HOME/v2t > ~/.v2t.""" + if env := os.environ.get("V2T_HOME"): + return Path(env).expanduser() + if xdg := os.environ.get("XDG_CONFIG_HOME"): + return Path(xdg).expanduser() / "v2t" + return Path.home() / ".v2t" + + +def config_path() -> Path: + return ( + Path(os.environ["V2T_CONFIG"]).expanduser() + if "V2T_CONFIG" in os.environ + else home() / "config.toml" + ) + + +def history_path() -> Path: + return home() / "history" / "transcriptions.jsonl" + + +def run_dir() -> Path: + return home() / "run" + + +def _private_dir(path: Path) -> Path: + path.mkdir(parents=True, exist_ok=True, mode=0o700) + path.chmod(0o700) + return path + + +def ensure_dirs() -> None: + _private_dir(home()) + _private_dir(history_path().parent) + _private_dir(run_dir()) + + +def _config_parent(path: Path) -> None: + """Create a config parent privately, without chmodding an existing custom directory.""" + if path.parent == home(): + _private_dir(path.parent) + return + existed = path.parent.exists() + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + if not existed: + path.parent.chmod(0o700) + + +def lock_path() -> Path: + return run_dir() / "v2t.lock" + + +def acquire_instance_lock(): + """Hold the single-instance lock for as long as the returned file stays open.""" + ensure_dirs() + handle = lock_path().open("a+") + os.chmod(handle.name, 0o600) + try: + fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + handle.close() + raise + handle.seek(0) + handle.truncate() + handle.write(str(os.getpid())) + handle.flush() + return handle + + +def running_pid() -> int | None: + """PID holding the instance lock, or None when v2t is not running.""" + path = lock_path() + if not path.exists(): + return None + with path.open("a+") as handle: + try: + fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + handle.seek(0) + raw = handle.read().strip() + return int(raw) if raw.isdigit() and int(raw) > 1 else None + fcntl.flock(handle, fcntl.LOCK_UN) + return None + + +def write_status(data: dict) -> None: + """Atomically write private runtime status for CLI and menu-bar clients.""" + directory = run_dir() + fd, temp_name = tempfile.mkstemp(dir=directory) + try: + os.fchmod(fd, 0o600) + with os.fdopen(fd, "w") as stream: + json.dump(data, stream) + os.replace(temp_name, directory / "status.json") + finally: + Path(temp_name).unlink(missing_ok=True) + + +def clear_status() -> None: + (run_dir() / "status.json").unlink(missing_ok=True) + + +def last_error_path() -> Path: + return run_dir() / "last-error" + + +def write_last_error(message: str) -> None: + """Remember a launch failure that happened before the runtime lock existed.""" + ensure_dirs() + path = last_error_path() + path.write_text(" ".join(message.split())) + path.chmod(0o600) + + +def read_last_error() -> str: + try: + return last_error_path().read_text().strip() + except OSError: + return "" + + +def clear_last_error() -> None: + last_error_path().unlink(missing_ok=True) + + +def read_status() -> dict | None: + """The running v2t's status, or None. Cleans stale or malformed state.""" + path = run_dir() / "status.json" + if not path.exists(): + return None + try: + data = json.loads(path.read_text()) + pid = data["pid"] + if not isinstance(pid, int) or pid <= 1: + raise ValueError("invalid pid") + except (TypeError, ValueError, KeyError, OSError): + path.unlink(missing_ok=True) + return None + if running_pid() != pid: + path.unlink(missing_ok=True) + return None + return data + + +# TOML section -> Config field. Flat dataclass, sectioned file: friendlier to edit. +_SECTIONS = { + "transcription": {"backend": "backend", "model": "stt_model"}, + "cleanup": { + "enabled": "cleanup_enabled", + "engine": "cleanup_engine", + "model": "cleanup_model", + "mode": "mode", + }, + "hotkey": {"key": "hotkey"}, + "audio": {"sample_rate": "sample_rate"}, + "behavior": {"pause_music": "pause_music", "save_history": "save_history"}, + "ollama": {"url": "ollama_url"}, +} + + +def load(overrides: dict | None = None) -> Config: + """Defaults < config.toml < CLI overrides. Unknown keys are ignored, not fatal.""" + cfg = Config() + path = config_path() + if path.exists(): + data = tomllib.loads(path.read_text()) + for section, mapping in _SECTIONS.items(): + for key, field in mapping.items(): + if key in data.get(section, {}): + setattr(cfg, field, data[section][key]) + for field, value in (overrides or {}).items(): + if value is not None: + setattr(cfg, field, value) + _validate(cfg) + return cfg + + +def _validate(cfg: Config) -> None: + choices = { + "backend": {"parakeet", "whisper"}, + "cleanup_engine": {"mlx", "ollama"}, + "mode": {"strict", "casual"}, + "hotkey": {"cmd_r", "cmd_l", "alt_r", "alt_l", "ctrl_r", "ctrl_l"}, + } + for field, allowed in choices.items(): + value = getattr(cfg, field) + if value not in allowed: + raise SystemExit( + f"invalid {field} {value!r}; choose: {', '.join(sorted(allowed))}" + ) + if not isinstance(cfg.sample_rate, int) or cfg.sample_rate <= 0: + raise SystemExit("audio.sample_rate must be a positive integer") + for field in ("cleanup_enabled", "pause_music", "save_history"): + if not isinstance(getattr(cfg, field), bool): + raise SystemExit(f"{field} must be true or false") + for field in ("stt_model", "cleanup_model", "ollama_url"): + if not isinstance(getattr(cfg, field), str): + raise SystemExit(f"{field} must be a string") + + +DEFAULT_TOML = """\ +# v2t config — every key is optional; delete what you don't override. + +[transcription] +backend = "parakeet" # parakeet (default, MLX) | whisper (needs voice2text[whisper]) +model = "" # blank = backend default (parakeet-tdt-0.6b-v3 / whisper-large-v3-turbo) + +[cleanup] +enabled = true +engine = "mlx" # mlx (in-process via mlx-lm, default) | ollama +model = "" # blank = engine default (Qwen2.5-1.5B-Instruct-4bit / qwen3:4b-instruct-2507) +mode = "strict" # strict (restructures) | casual (punctuation + fillers only) + +[hotkey] +key = "cmd_r" # cmd_r | cmd_l | alt_r | alt_l | ctrl_r | ctrl_l + +[audio] +sample_rate = 16000 + +[behavior] +pause_music = false +save_history = true # append every transcription to history/transcriptions.jsonl + +[ollama] +url = "http://localhost:11434" +""" + + +def write_default(path: Path | None = None) -> Path: + """Write a commented template if absent. Never clobbers an existing file.""" + path = path or config_path() + _config_parent(path) + if not path.exists(): + path.write_text(DEFAULT_TOML) + path.chmod(0o600) + return path + + +def write_config(text: str, path: Path | None = None) -> Path: + """Write an explicit user config privately, preserving custom parent permissions.""" + path = path or config_path() + _config_parent(path) + path.write_text(text) + path.chmod(0o600) + return path + + +def append_history(record: dict) -> None: + """Append one private JSONL record.""" + path = history_path() + _private_dir(path.parent) + record = {"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), **record} + fd = os.open(path, os.O_APPEND | os.O_CREAT | os.O_WRONLY, 0o600) + os.fchmod(fd, 0o600) + with os.fdopen(fd, "a") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + +if __name__ == "__main__": + # ponytail: one runnable check for the trust-boundary logic (paths + merge + io). + import tempfile + + with tempfile.TemporaryDirectory() as d: + os.environ["V2T_HOME"] = d + os.environ.pop("XDG_CONFIG_HOME", None) + os.environ.pop("V2T_CONFIG", None) + + assert home() == Path(d), "V2T_HOME must win" + assert load().backend == "parakeet", "default backend" + assert load({"mode": "casual"}).mode == "casual", "override wins" + assert load({"mode": None}).mode == "strict", "None override ignored" + + p = write_default() + assert p.exists() and "qwen3" in p.read_text(), "template written" + before = p.read_text() + write_default() + assert p.read_text() == before, "never clobbers existing config" + # config.toml round-trips through the loader + assert load().cleanup_engine == "mlx", "toml parsed" + + append_history({"raw": "héllo", "clean": "Hello."}) + line = json.loads(history_path().read_text().splitlines()[-1]) + assert line["clean"] == "Hello." and line["ts"].endswith("+00:00"), ( + "history roundtrip" + ) + + os.environ.pop("V2T_HOME") + os.environ["XDG_CONFIG_HOME"] = d + assert home() == Path(d) / "v2t", "XDG fallback" + + print("config.py: all checks passed") diff --git a/v2t/menubar.py b/v2t/menubar.py new file mode 100644 index 0000000..6a20722 --- /dev/null +++ b/v2t/menubar.py @@ -0,0 +1,135 @@ +"""Build and launch the optional one-file macOS menu-bar app.""" + +from __future__ import annotations + +import fcntl +import os +import plistlib +import re +import shutil +import subprocess +import sys +import tempfile +from importlib.resources import as_file, files +from pathlib import Path + +from . import __version__, config + +APP_NAME = "Voice2Text.app" +BUNDLE_ID = "com.lucharo.voice2text" + + +def app_path() -> Path: + return Path.home() / "Applications" / APP_NAME + + +def app_executable() -> Path: + return app_path() / "Contents" / "MacOS" / "Voice2Text" + + +def installed() -> bool: + return app_executable().is_file() + + +def running() -> bool: + """Whether the menu app owns its single-instance lock.""" + path = config.run_dir() / "menubar.lock" + if not path.exists(): + return False + with path.open("a+") as handle: + try: + fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + return True + fcntl.flock(handle, fcntl.LOCK_UN) + return False + + +def signing_identity() -> str: + """Use a stable local development identity when one is already available.""" + result = subprocess.run( + ["security", "find-identity", "-v", "-p", "codesigning"], + capture_output=True, + text=True, + ) + identities = re.findall(r'"([^"]+)"', result.stdout) + return next( + (identity for identity in identities if identity.startswith("Apple Development:")), + "-", + ) + + +def install() -> Path: + """Compile the bundled Swift source into a small, grantable app bundle.""" + if sys.platform != "darwin": + raise SystemExit("the Voice2Text menu app is macOS-only") + if running(): + raise SystemExit("quit Voice2Text before updating the menu app") + destination = app_path() + destination.parent.mkdir(parents=True, exist_ok=True) + source = files("v2t").joinpath("native", "Voice2Text.swift") + with as_file(source) as source_path, tempfile.TemporaryDirectory( + dir=destination.parent + ) as temporary: + bundle = Path(temporary) / APP_NAME + contents = bundle / "Contents" + executable = contents / "MacOS" / "Voice2Text" + executable.parent.mkdir(parents=True) + info = { + "CFBundleDevelopmentRegion": "en", + "CFBundleExecutable": "Voice2Text", + "CFBundleIdentifier": BUNDLE_ID, + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "Voice2Text", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": __version__, + "CFBundleVersion": "1", + "LSMinimumSystemVersion": "13.0", + "LSUIElement": True, + "NSMicrophoneUsageDescription": "Voice2Text uses the microphone for fully local transcription.", + "NSPrincipalClass": "NSApplication", + "V2THome": str(config.home()), + "V2TPythonExecutable": sys.executable, + } + if custom_config := os.environ.get("V2T_CONFIG"): + info["V2TConfig"] = str(Path(custom_config).expanduser()) + (contents / "Info.plist").write_bytes(plistlib.dumps(info)) + subprocess.run( + [ + "xcrun", + "swiftc", + "-parse-as-library", + str(source_path), + "-o", + str(executable), + "-framework", + "AppKit", + "-framework", + "AVFoundation", + "-framework", + "ApplicationServices", + ], + check=True, + ) + executable.chmod(0o755) + subprocess.run( + [ + "codesign", + "--force", + "--deep", + "--sign", + signing_identity(), + str(bundle), + ], + check=True, + ) + if destination.exists(): + shutil.rmtree(destination) + shutil.move(bundle, destination) + return destination + + +def open_app() -> None: + if not installed(): + raise SystemExit("menu app is not installed; run: v2t menubar install") + subprocess.run(["open", str(app_path())], check=True) diff --git a/v2t/native/Voice2Text.swift b/v2t/native/Voice2Text.swift new file mode 100644 index 0000000..edbd4aa --- /dev/null +++ b/v2t/native/Voice2Text.swift @@ -0,0 +1,307 @@ +import AppKit +import ApplicationServices +import AVFoundation + +@main +struct Voice2TextApp { + static func main() { + let app = NSApplication.shared + let delegate = Voice2TextMenu() + app.delegate = delegate + app.setActivationPolicy(.accessory) + app.run() + } +} + +final class Voice2TextMenu: NSObject, NSApplicationDelegate { + private let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + private let menu = NSMenu() + private var engine: Process? + private var timer: Timer? + private var phase = "off" + private var status: [String: Any] = [:] + private var externalEngine = false + private var lockFD: Int32 = -1 + private var logHandle: FileHandle? + private var rendered = "" + private var terminationPending = false + + private var home: URL { + let value = Bundle.main.object(forInfoDictionaryKey: "V2THome") as? String + return URL(fileURLWithPath: value ?? NSHomeDirectory() + "/.v2t") + } + + func applicationDidFinishLaunching(_ notification: Notification) { + guard acquireAppLock() else { + NSApp.terminate(nil) + return + } + item.menu = menu + render() + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + self?.refresh() + } + if CommandLine.arguments.contains("--start") { + start() + } + } + + func applicationWillTerminate(_ notification: Notification) { + timer?.invalidate() + engine?.terminate() + logHandle?.closeFile() + logHandle = nil + if lockFD >= 0 { close(lockFD) } + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + guard let engine else { return .terminateNow } + terminationPending = true + phase = "stopping" + render() + engine.terminate() + return .terminateLater + } + + private func acquireAppLock() -> Bool { + let run = home.appendingPathComponent("run") + try? FileManager.default.createDirectory(at: run, withIntermediateDirectories: true) + chmod(home.path, 0o700) + chmod(run.path, 0o700) + lockFD = open(run.appendingPathComponent("menubar.lock").path, O_CREAT | O_RDWR, 0o600) + if lockFD >= 0 { fchmod(lockFD, 0o600) } + return lockFD >= 0 && flock(lockFD, LOCK_EX | LOCK_NB) == 0 + } + + @objc private func start() { + guard engine == nil && !externalEngine && phase != "permissions" && phase != "starting" else { return } + NSApp.activate(ignoringOtherApps: true) + try? FileManager.default.removeItem(at: home.appendingPathComponent("run/last-error")) + phase = "permissions" + render() + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + requestSystemPermissions() + case .notDetermined: + AVCaptureDevice.requestAccess(for: .audio) { [weak self] granted in + DispatchQueue.main.async { + if granted { self?.requestSystemPermissions() } + else { self?.phase = "permission-error"; self?.render() } + } + } + default: + phase = "permission-error" + render() + } + } + + private func requestSystemPermissions() { + let prompt = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary + let accessibility = AXIsProcessTrustedWithOptions(prompt) + guard accessibility else { + phase = "permission-error" + render() + return + } + launchEngine() + } + + private func launchEngine() { + guard let python = Bundle.main.object(forInfoDictionaryKey: "V2TPythonExecutable") as? String else { + phase = "error" + render() + return + } + let process = Process() + process.executableURL = URL(fileURLWithPath: python) + process.arguments = ["-m", "v2t"] + var environment = ProcessInfo.processInfo.environment + environment["V2T_HOME"] = home.path + environment["V2T_LAUNCH_CONTEXT"] = "menubar" + let toolBin = URL(fileURLWithPath: python).deletingLastPathComponent().path + let inheritedPath = environment["PATH"] ?? "/usr/bin:/bin" + environment["PATH"] = "\(toolBin):/opt/homebrew/bin:/usr/local/bin:\(inheritedPath)" + if let path = Bundle.main.object(forInfoDictionaryKey: "V2TConfig") as? String { + environment["V2T_CONFIG"] = path + } + process.environment = environment + let log = home.appendingPathComponent("run/v2t.log") + if let size = try? log.resourceValues(forKeys: [.fileSizeKey]).fileSize, size > 1_048_576 { + let previous = log.deletingPathExtension().appendingPathExtension("log.1") + try? FileManager.default.removeItem(at: previous) + try? FileManager.default.moveItem(at: log, to: previous) + } + let logFD = open(log.path, O_WRONLY | O_CREAT | O_APPEND, 0o600) + if logFD >= 0 { + fchmod(logFD, 0o600) + let handle = FileHandle(fileDescriptor: logFD, closeOnDealloc: true) + logHandle = handle + process.standardOutput = handle + process.standardError = handle + } + process.terminationHandler = { [weak self] _ in + DispatchQueue.main.async { + guard let self else { return } + let shouldQuit = self.terminationPending + self.engine = nil + self.logHandle?.closeFile() + self.logHandle = nil + self.phase = "off" + if shouldQuit { + NSApp.reply(toApplicationShouldTerminate: true) + } else { + self.refresh() + } + } + } + do { + phase = "starting" + render() + engine = process + try process.run() + } catch { + engine = nil + phase = "error" + render() + } + } + + @objc private func stop() { + engine?.terminate() + phase = "stopping" + render() + } + + private func refresh() { + let url = home.appendingPathComponent("run/status.json") + var live = false + if let data = try? Data(contentsOf: url), + let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let pid = value["pid"] as? Int, engineOwnsLock(pid) { + live = true + status = value + externalEngine = engine == nil + if phase != "stopping" { + phase = value["state"] as? String ?? phase + } + } + } + if !live && engine == nil && phase != "permission-error" && phase != "error" { + phase = "off" + status = [:] + externalEngine = false + try? FileManager.default.removeItem(at: url) + } + if engine == nil, let message = try? String(contentsOf: home.appendingPathComponent("run/last-error"), encoding: .utf8), !message.isEmpty { + phase = "error" + } + render() + } + + private func engineOwnsLock(_ pid: Int) -> Bool { + let path = home.appendingPathComponent("run/v2t.lock").path + let fd = open(path, O_RDWR) + guard fd >= 0 else { return false } + defer { close(fd) } + if flock(fd, LOCK_EX | LOCK_NB) == 0 { + flock(fd, LOCK_UN) + return false + } + let owner = try? String(contentsOfFile: path, encoding: .utf8) + .trimmingCharacters(in: .whitespacesAndNewlines) + return owner == String(pid) + } + + private func render() { + let microphone = microphoneGranted + let accessibility = AXIsProcessTrusted() + let stt = status["stt"] as? String ?? "" + let cleanup = status["cleanup"] as? String ?? "" + let signature = "\(phase)|\(stt)|\(cleanup)|\(engine != nil)|\(externalEngine)|\(microphone)|\(accessibility)" + guard rendered != signature else { return } + rendered = signature + let presentation: (String, String) = switch phase { + case "permissions": ("hourglass", "Checking permissions…") + case "starting", "loading-stt": ("hourglass", "Loading transcription model…") + case "loading-cleanup": ("hourglass", "Loading cleanup model…") + case "idle": ("waveform", "Ready") + case "recording": ("waveform.circle.fill", "Recording…") + case "transcribing": ("ellipsis.circle", "Transcribing…") + case "cleaning": ("ellipsis.circle", "Cleaning up…") + case "stopping": ("hourglass", "Stopping…") + case "permission-error": ("exclamationmark.triangle", "Permissions required") + case "error": ("exclamationmark.triangle", "Could not start — open Log") + default: ("waveform.slash", "Off") + } + let icon = NSImage(systemSymbolName: presentation.0, accessibilityDescription: presentation.1) + ?? NSImage(systemSymbolName: "waveform", accessibilityDescription: presentation.1) + icon?.isTemplate = true + item.button?.image = icon + item.button?.imagePosition = .imageOnly + item.button?.title = "" + item.button?.toolTip = presentation.1 + menu.removeAllItems() + add(presentation.1, enabled: false) + if !stt.isEmpty && !cleanup.isEmpty { + add("\(stt) · \(cleanup == "off" ? "no cleanup" : "clean: \(cleanup)")", enabled: false) + } + menu.addItem(.separator()) + if externalEngine { add("Running from terminal", enabled: false) } + else if phase == "permissions" || phase == "starting" { add("Starting…", enabled: false) } + else if phase == "stopping" { add("Stopping…", enabled: false) } + else if engine == nil { add("Start v2t", action: #selector(start)) } + else { add("Stop v2t", action: #selector(stop)) } + menu.addItem(.separator()) + add(permissionLabel("Microphone", microphone), action: #selector(openMicrophone)) + add(permissionLabel("Accessibility", accessibility), action: #selector(openAccessibility)) + menu.addItem(.separator()) + add("Config", action: #selector(openConfig)) + add("Transcription History", action: #selector(openHistory)) + add("Log", action: #selector(openLog)) + menu.addItem(.separator()) + add("Quit Voice2Text", action: #selector(quit)) + } + + private var microphoneGranted: Bool { + AVCaptureDevice.authorizationStatus(for: .audio) == .authorized + } + + private func permissionLabel(_ name: String, _ granted: Bool) -> String { + "\(granted ? "✓" : "○") \(name) · \(granted ? "Granted" : "Click to grant")" + } + + private func add(_ title: String, action: Selector? = nil, enabled: Bool = true) { + let row = NSMenuItem(title: title, action: action, keyEquivalent: "") + row.target = self + row.isEnabled = enabled + menu.addItem(row) + } + + private func openPane(_ pane: String) { + let base = "x-apple.systempreferences:com.apple.preference.security?" + if let url = URL(string: base + pane) { NSWorkspace.shared.open(url) } + } + + @objc private func openMicrophone() { + NSApp.activate(ignoringOtherApps: true) + if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined { + AVCaptureDevice.requestAccess(for: .audio) { [weak self] _ in + DispatchQueue.main.async { self?.rendered = ""; self?.render() } + } + } else { + openPane("Privacy_Microphone") + } + } + + @objc private func openAccessibility() { + let prompt = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary + _ = AXIsProcessTrustedWithOptions(prompt) + rendered = "" + render() + } + + @objc private func openConfig() { NSWorkspace.shared.open(home) } + @objc private func openHistory() { NSWorkspace.shared.open(home.appendingPathComponent("history")) } + @objc private func openLog() { NSWorkspace.shared.open(home.appendingPathComponent("run/v2t.log")) } + @objc private func quit() { NSApp.terminate(nil) } +} diff --git a/v2t/permissions.py b/v2t/permissions.py new file mode 100644 index 0000000..1d4e70b --- /dev/null +++ b/v2t/permissions.py @@ -0,0 +1,56 @@ +"""Read the two macOS privacy grants v2t needs, without prompting.""" + +from __future__ import annotations + +import sys +import threading + + +def statuses() -> dict[str, str]: + if sys.platform != "darwin": + return {"microphone": "unknown", "accessibility": "unknown"} + + from ApplicationServices import AXIsProcessTrusted + + accessibility = "granted" if AXIsProcessTrusted() else "missing" + try: + from AVFoundation import AVCaptureDevice, AVMediaTypeAudio + + microphone = { + 0: "not-requested", + 1: "restricted", + 2: "denied", + 3: "granted", + }.get( + int(AVCaptureDevice.authorizationStatusForMediaType_(AVMediaTypeAudio)), + "unknown", + ) + except ImportError: + microphone = "unknown" + return { + "microphone": microphone, + "accessibility": accessibility, + } + + +def request_microphone(timeout: float = 60) -> bool: + """Ask macOS for microphone access once, waiting for the user's choice.""" + from AVFoundation import AVCaptureDevice, AVMediaTypeAudio + + current = int(AVCaptureDevice.authorizationStatusForMediaType_(AVMediaTypeAudio)) + if current != 0: + return current == 3 + + answered = threading.Event() + granted = False + + def complete(value): + nonlocal granted + granted = bool(value) + answered.set() + + AVCaptureDevice.requestAccessForMediaType_completionHandler_( + AVMediaTypeAudio, complete + ) + answered.wait(timeout) + return granted diff --git a/v2t/service.py b/v2t/service.py new file mode 100644 index 0000000..b6abbad --- /dev/null +++ b/v2t/service.py @@ -0,0 +1,275 @@ +"""Optional per-user launchd service for keeping v2t warm between sessions.""" + +from __future__ import annotations + +import os +import plistlib +import signal +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +from . import config, menubar + +LABEL = menubar.BUNDLE_ID +READY_STATES = {"idle", "recording", "transcribing", "cleaning"} +START_TIMEOUT = 120 +STOP_TIMEOUT = 120 +FORCE_TIMEOUT = 5 + + +def plist_path() -> Path: + return Path.home() / "Library/LaunchAgents" / f"{LABEL}.plist" + + +def target() -> str: + return f"gui/{os.getuid()}/{LABEL}" + + +def _launchctl(*args: str, check: bool = True) -> subprocess.CompletedProcess: + result = subprocess.run( + ["launchctl", *args], capture_output=True, text=True, check=False + ) + if check and result.returncode: + raise RuntimeError( + result.stderr.strip() or result.stdout.strip() or "launchctl failed" + ) + return result + + +def loaded() -> bool: + return _launchctl("print", target(), check=False).returncode == 0 + + +def service_pid() -> int | None: + """PID owned by launchd for this job, or None when the job is inactive.""" + result = _launchctl("print", target(), check=False) + if result.returncode: + return None + for line in result.stdout.splitlines(): + key, separator, value = line.strip().partition(" = ") + if key == "pid" and separator and value.isdigit(): + return int(value) + return None + + +def engine_ready(menu_pid: int | None = None) -> bool: + status = config.read_status() + if not status or status.get("state") not in READY_STATES: + return False + if menu_pid is None: + return True + engine_pid = status.get("pid") + return isinstance(engine_pid, int) and _is_child(engine_pid, menu_pid) + + +def _is_child(pid: int, parent_pid: int) -> bool: + result = subprocess.run( + ["ps", "-o", "ppid=", "-p", str(pid)], + capture_output=True, + text=True, + check=False, + ) + parent = result.stdout.strip() + return parent.isdigit() and int(parent) == parent_pid + + +def owned_engine_pid(menu_pid: int) -> int | None: + """Return the live v2t engine only when it is a child of this menu process.""" + engine_pid = config.running_pid() + if engine_pid is not None and _is_child(engine_pid, menu_pid): + return engine_pid + result = subprocess.run( + ["pgrep", "-P", str(menu_pid)], + capture_output=True, + text=True, + check=False, + ) + return next( + (int(line) for line in result.stdout.splitlines() if line.strip().isdigit()), + None, + ) + + +def _pid_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +def plist_data() -> dict: + log = config.run_dir() / "v2t.log" + return { + "Label": LABEL, + "ProgramArguments": [str(menubar.app_executable()), "--start"], + "RunAtLoad": True, + "StandardOutPath": str(log), + "StandardErrorPath": str(log), + "Umask": 0o077, + } + + +def _prepare_log() -> None: + config.ensure_dirs() + log = config.run_dir() / "v2t.log" + log.touch() + log.chmod(0o600) + + +def install() -> Path: + if sys.platform != "darwin": + raise SystemExit("the v2t service is macOS-only") + if not menubar.installed(): + raise SystemExit("menu app is not installed; run: v2t menubar install") + if menubar.running() and service_pid() is None: + raise SystemExit("quit Voice2Text before installing the login service") + if config.running_pid() is not None and service_pid() is None: + raise SystemExit( + "v2t is already running outside the login service; stop it first" + ) + if loaded(): + stop() + _launchctl("bootout", target()) + _prepare_log() + path = plist_path() + path.parent.mkdir(parents=True, exist_ok=True) + fd, temp_name = tempfile.mkstemp(dir=path.parent) + try: + os.fchmod(fd, 0o600) + with os.fdopen(fd, "wb") as stream: + plistlib.dump(plist_data(), stream) + os.replace(temp_name, path) + finally: + Path(temp_name).unlink(missing_ok=True) + _launchctl("bootstrap", f"gui/{os.getuid()}", str(path)) + return path + + +def start() -> None: + path = plist_path() + if not path.exists(): + raise SystemExit("service is not installed; run: v2t service install") + menu_pid = service_pid() + if menu_pid is not None: + if engine_ready(menu_pid): + return + engine_pid = config.running_pid() + if engine_pid is not None and not _is_child(engine_pid, menu_pid): + raise SystemExit( + "v2t is running outside the login service; stop it first" + ) + elif menubar.running(): + raise SystemExit("quit Voice2Text before starting the login service") + if menu_pid is None and config.running_pid() is not None: + raise SystemExit( + "v2t is already running outside the login service; stop it first" + ) + _prepare_log() + menu_engine_pid = ( + owned_engine_pid(menu_pid) if menu_pid is not None else None + ) + restarting = menu_pid is not None and menu_engine_pid is None + if menu_pid is None or restarting: + config.clear_last_error() + if restarting: + stop() + _launchctl("kickstart", target()) + elif loaded(): + _launchctl("kickstart", target()) + else: + _launchctl("bootstrap", f"gui/{os.getuid()}", str(path)) + seen_service = menu_pid is not None and not restarting + deadline = time.monotonic() + START_TIMEOUT + while time.monotonic() < deadline: + current_service = service_pid() + if current_service is not None and engine_ready(current_service): + return + if error := config.read_last_error(): + raise RuntimeError(error) + if current_service is not None: + seen_service = True + elif seen_service: + raise RuntimeError( + f"service stopped during startup; check {config.run_dir() / 'v2t.log'}" + ) + time.sleep(0.1) + if not seen_service: + raise RuntimeError( + f"service did not start; check {config.run_dir() / 'v2t.log'}" + ) + raise RuntimeError( + f"models did not become ready within {START_TIMEOUT}s; " + f"check {config.run_dir() / 'v2t.log'}" + ) + + +def stop() -> None: + menu_pid = service_pid() + if menu_pid is None: + return + engine_pid = owned_engine_pid(menu_pid) + if engine_pid is not None: + deadline = time.monotonic() + STOP_TIMEOUT + runtime = config.read_status() + already_stopping = bool( + runtime + and runtime.get("pid") == engine_pid + and runtime.get("state") == "stopping" + ) + if not already_stopping: + try: + os.kill(engine_pid, signal.SIGTERM) + except ProcessLookupError: + pass + while time.monotonic() < deadline: + if not _pid_alive(engine_pid): + break + time.sleep(0.1) + else: + try: + os.kill(engine_pid, signal.SIGKILL) + except ProcessLookupError: + pass + force_deadline = time.monotonic() + FORCE_TIMEOUT + while time.monotonic() < force_deadline and _pid_alive(engine_pid): + time.sleep(0.1) + if _pid_alive(engine_pid): + raise RuntimeError( + f"engine did not stop; check {config.run_dir() / 'v2t.log'}" + ) + _launchctl("kill", "SIGTERM", target()) + deadline = time.monotonic() + STOP_TIMEOUT + while time.monotonic() < deadline: + if service_pid() is None: + return + time.sleep(0.1) + raise RuntimeError(f"service is still stopping; check {config.run_dir() / 'v2t.log'}") + + +def uninstall() -> None: + if loaded(): + stop() + _launchctl("bootout", target()) + plist_path().unlink(missing_ok=True) + + +def status() -> str: + if not plist_path().exists(): + return "not installed" + menu_pid = service_pid() + engine_pid = config.running_pid() + if menu_pid is not None: + if engine_pid is not None and _is_child(engine_pid, menu_pid): + return "running" + if engine_pid is not None: + return "menu running; v2t is running outside the login service" + return "menu running; v2t off" + if engine_pid is not None: + return "installed; v2t is running outside the login service" + return "loaded" if loaded() else "installed but unloaded" diff --git a/voice2text.py b/voice2text.py deleted file mode 100644 index d9abed8..0000000 --- a/voice2text.py +++ /dev/null @@ -1,291 +0,0 @@ -""" -Voice-to-text with push-to-talk hotkey. - -Hold Right Command to record, release to transcribe and paste. - -Prerequisites: - brew install ollama - ollama pull qwen2.5:3b -""" - -import subprocess -import tempfile -import threading -import sys -import time -import argparse -import numpy as np -import sounddevice as sd -import mlx_whisper -from scipy.io import wavfile -from pynput import keyboard -from loguru import logger - -# Config -SAMPLE_RATE = 16000 -WHISPER_MODEL = "mlx-community/whisper-large-v3-turbo" -OLLAMA_MODEL = "qwen2.5:3b" -PUSH_TO_TALK_KEY = keyboard.Key.cmd_r - -CLEANUP_PROMPT_STRICT = """Clean up this transcription. Fix punctuation, remove filler words (um, uh, like, you know), fix obvious mishearings, keep the meaning intact. Output ONLY the cleaned text, nothing else: - -{text}""" - -CLEANUP_PROMPT_CASUAL = """Lightly clean up this transcription. Only fix punctuation and remove filler words (um, uh, like, you know). Do NOT restructure sentences or change word order. Keep the original phrasing. Output ONLY the cleaned text, nothing else: - -{text}""" - - -def check_and_request_permissions(): - """Check for required permissions and open System Settings if needed.""" - logger.info("Checking permissions...") - - test_result = subprocess.run( - ["osascript", "-e", 'tell application "System Events" to return "ok"'], - capture_output=True, - text=True - ) - - if "not allowed" in test_result.stderr.lower() or test_result.returncode != 0: - logger.warning("Permissions needed!") - logger.info("Grant permissions to your TERMINAL APP (Terminal, iTerm, Ghostty, VS Code, etc.)") - - logger.info("Opening Accessibility settings...") - subprocess.run([ - "open", - "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - ]) - - input("Press Enter after granting Accessibility permission...") - - logger.info("Opening Input Monitoring settings...") - subprocess.run([ - "open", - "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent" - ]) - - input("Press Enter after granting Input Monitoring permission...") - - logger.success("Permissions granted. You may need to restart your terminal, then run this script again.") - sys.exit(0) - - logger.success("Permissions OK") - - -class VoiceToText: - def __init__(self, pause_music: bool = False, casual: bool = False): - self.recording = False - self.frames: list[np.ndarray] = [] - self.stream = None - self.processing = False - self.record_start = 0.0 - self.pause_music = pause_music - self.casual = casual - self.was_playing = False - - def audio_callback(self, indata, frame_count, time_info, status): - if self.recording: - self.frames.append(indata.copy()) - - def start_recording(self): - if self.recording or self.processing: - return - - self.recording = True - self.frames = [] - self.record_start = time.perf_counter() - - if self.pause_music: - # Check if something is playing before pausing - result = subprocess.run( - ["nowplaying-cli", "get", "playbackRate"], - capture_output=True, - text=True - ) - self.was_playing = result.stdout.strip() == "1" - if self.was_playing: - subprocess.run(["nowplaying-cli", "pause"]) - - logger.info("Recording...") - - self.stream = sd.InputStream( - samplerate=SAMPLE_RATE, - channels=1, - dtype="float32", - callback=self.audio_callback - ) - self.stream.start() - - def stop_recording(self): - if not self.recording: - return - - self.recording = False - duration = time.perf_counter() - self.record_start - if self.stream: - self.stream.stop() - self.stream.close() - self.stream = None - - logger.info(f"Stopped ({duration:.1f}s)") - - if self.frames: - threading.Thread(target=self.process_audio, daemon=True).start() - - def process_audio(self): - self.processing = True - - try: - audio = np.concatenate(self.frames, axis=0) - - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: - wavfile.write(f.name, SAMPLE_RATE, (audio * 32767).astype(np.int16)) - temp_path = f.name - - logger.info("Transcribing...") - t0 = time.perf_counter() - result = mlx_whisper.transcribe(temp_path, path_or_hf_repo=WHISPER_MODEL) - raw_text = result["text"].strip() - t1 = time.perf_counter() - logger.info(f"Raw: {raw_text} ({t1-t0:.2f}s)") - - if not raw_text: - logger.warning("No speech detected") - return - - logger.info("Cleaning up...") - t0 = time.perf_counter() - prompt_template = CLEANUP_PROMPT_CASUAL if self.casual else CLEANUP_PROMPT_STRICT - prompt = prompt_template.format(text=raw_text) - - try: - result = subprocess.run( - ["ollama", "run", OLLAMA_MODEL, prompt], - capture_output=True, - text=True, - timeout=30, - ) - if result.returncode != 0: - raise Exception(f"Ollama exited with code {result.returncode}: {result.stderr}") - cleaned_text = result.stdout.strip() - if not cleaned_text: - raise Exception("Ollama returned empty response") - t1 = time.perf_counter() - logger.info(f"Clean: {cleaned_text} ({t1-t0:.2f}s)") - except Exception as e: - logger.error(f"LLM cleanup failed: {e}") - logger.warning("Falling back to raw transcription") - cleaned_text = raw_text - - self.paste_to_cursor(cleaned_text) - logger.success("Pasted!") - - finally: - if self.pause_music and self.was_playing: - subprocess.run(["nowplaying-cli", "play"]) - - self.processing = False - - def paste_to_cursor(self, text: str) -> None: - """Copy to clipboard, paste at cursor, then restore original clipboard.""" - original = subprocess.run(["pbpaste"], capture_output=True, text=True).stdout - - subprocess.run(["pbcopy"], input=text, text=True) - subprocess.run([ - "osascript", "-e", - 'tell application "System Events" to keystroke "v" using command down' - ]) - - time.sleep(0.15) - subprocess.run(["pbcopy"], input=original, text=True) - - def on_press(self, key): - if key == PUSH_TO_TALK_KEY: - self.start_recording() - - def on_release(self, key): - if key == PUSH_TO_TALK_KEY: - self.stop_recording() - - def run(self): - mode = "Casual" if self.casual else "Strict" - logger.info(f"Voice-to-Text Started - {mode} Mode") - if self.pause_music: - logger.info("Pause Music - Turned On") - logger.info("Hold Right Command to record, release to transcribe and paste. Ctrl+C to quit.") - logger.warning("Note: Right Command is a modifier key - you cannot type while holding it") - - try: - with keyboard.Listener( - on_press=self.on_press, - on_release=self.on_release - ) as listener: - listener.join() - except KeyboardInterrupt: - logger.info("Shutting down...") - sys.exit(0) - - -def main(): - parser = argparse.ArgumentParser(description="voice2text: push-to-talk transcription") - parser.add_argument( - "--pause-music", - action="store_true", - help="Pause Music/Spotify while recording, resume after paste" - ) - parser.add_argument( - "--casual", - action="store_true", - help="Light cleanup only (punctuation + filler words). Won't restructure sentences." - ) - args = parser.parse_args() - - if args.pause_music: - nowplaying_check = subprocess.run(["which", "nowplaying-cli"], capture_output=True) - if nowplaying_check.returncode != 0: - logger.warning("nowplaying-cli not found. Install with: brew install nowplaying-cli") - logger.warning("Music pause feature disabled.") - args.pause_music = False - - ollama_check = subprocess.run(["which", "ollama"], capture_output=True) - if ollama_check.returncode != 0: - logger.error("Ollama not found. Install with: brew install ollama") - sys.exit(1) - - model_check = subprocess.run( - ["ollama", "list"], - capture_output=True, - text=True - ) - if OLLAMA_MODEL.split(":")[0] not in model_check.stdout: - logger.error(f"Model not found. Pull with: ollama pull {OLLAMA_MODEL}") - sys.exit(1) - - check_and_request_permissions() - - try: - logger.info("Loading models...") - - t0 = time.perf_counter() - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: - silence = np.zeros(SAMPLE_RATE, dtype=np.int16) - wavfile.write(f.name, SAMPLE_RATE, silence) - mlx_whisper.transcribe(f.name, path_or_hf_repo=WHISPER_MODEL) - logger.success(f"Whisper model ready ({time.perf_counter()-t0:.1f}s)") - - t0 = time.perf_counter() - subprocess.run( - ["ollama", "run", OLLAMA_MODEL, "hi"], - capture_output=True, text=True, timeout=60, - ) - logger.success(f"Ollama model ready ({time.perf_counter()-t0:.1f}s)") - - app = VoiceToText(pause_music=args.pause_music, casual=args.casual) - app.run() - except KeyboardInterrupt: - logger.info("Shutting down...") - sys.exit(0) - - -if __name__ == "__main__": - main()