diff --git a/README.md b/README.md index 87e510d2..e44ba798 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ C/C++ speech-to-text inference library. Runs diverse STT model families via [GGU | Voxtral | `voxtral-mini-3b-2507`, `voxtral-small-24b-2507` (audio-LLM; transcription + translation) | [docs/models/voxtral.md](docs/models/voxtral.md) | | Voxtral Realtime | `voxtral-mini-4b-realtime-2602` (streaming audio-LLM) | [docs/models/voxtral-realtime.md](docs/models/voxtral-realtime.md) | | MedASR | `medasr` (Conformer + CTC, English medical-dictation, gated) | [docs/models/medasr.md](docs/models/medasr.md) | +| MOSS Transcribe-Diarize | `moss-transcribe-diarize` (audio-LLM; English + Chinese ASR with inline speaker diarization) | [docs/models/moss-transcribe-diarize.md](docs/models/moss-transcribe-diarize.md) | Per-variant model cards live under [`docs/models/`](docs/models/). diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 40d00f27..9797efc0 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -29,7 +29,7 @@ of tests. | `TRANSCRIBE_CONV_DIRECT_DW` / `TRANSCRIBE_CONV_NO_DIRECT_DW` | Force the depthwise-conv dispatch to the direct `conv_2d_dw` path / the im2col path, overriding the per-family backend default. | | `TRANSCRIBE_CONV_DIRECT_PW` / `TRANSCRIBE_CONV_NO_DIRECT_PW` | Force the pointwise-conv dispatch to direct `mul_mat` / im2col, overriding the backend default. | | `TRANSCRIBE_DUMP_DIR=` | Enable the per-stage tensor dumper; writes `.f32` + `.json` per dumped tensor into ``. The basis for the numerical-comparison harness (`scripts/compare_tensors.py`). | -| `TRANSCRIBE_PERF_DEBUG` | Print a per-stage timing breakdown to stderr (DEBUG log) on the families that profile (`cohere`, `granite`, `canary`, `canary_qwen`, `moonshine`, `moonshine_streaming`, `qwen3_asr`, `whisper`). For whisper, a value containing `cpu` or `all` additionally prints the CPU sub-section breakdown. | +| `TRANSCRIBE_PERF_DEBUG` | Print a per-stage timing breakdown to stderr (DEBUG log) on the families that profile (`cohere`, `granite`, `canary`, `canary_qwen`, `moonshine`, `moonshine_streaming`, `moss`, `qwen3_asr`, `whisper`). For whisper, a value containing `cpu` or `all` additionally prints the CPU sub-section breakdown. | | `TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING` | Print a per-component streaming wall-time breakdown at stream finalize (voxtral_realtime). | | `TRANSCRIBE_TEST_DEV_INIT_THROW=` | Fault injection: backend device init (`ggml_backend_dev_init`) throws for devices whose name contains `` (`*` matches every device). Exercises throw → skip → CPU-fallback in backend probing; an explicit backend request fails with `TRANSCRIBE_ERR_BACKEND`. Used by `backend_init_throw_unit` and `scripts/ci/vulkan_degradation_check.py`. | | `TRANSCRIBE_TEST_TEARDOWN_THROW` | Fault injection: any non-empty value injects a throw after each real free inside the `transcribe::safe_*` teardown wrappers, proving containment without leaking the handle. Used by `teardown_safety_unit`. | diff --git a/docs/models/moss-transcribe-diarize.md b/docs/models/moss-transcribe-diarize.md new file mode 100644 index 00000000..36dface9 --- /dev/null +++ b/docs/models/moss-transcribe-diarize.md @@ -0,0 +1,226 @@ +# MOSS-Transcribe-Diarize + +OpenMOSS's [`OpenMOSS-Team/MOSS-Transcribe-Diarize`](https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize) +ported to transcribe.cpp. A 0.9B audio-LLM: a 24-layer Whisper-Medium audio +encoder (`d_model=1024`, GELU, LayerNorm) feeds a 4x temporal merge +(1024 -> 4096) and a VQAdaptor MLP bridge into a Qwen3-0.6B causal decoder +(28 layers, `hidden_size=1024`, GQA 16/8 heads, `rope_theta=1e6`) via +audio-token injection at `<|audio_pad|>` positions. + +## What it's for + +Offline English and Chinese speech-to-text with speaker diarization. Takes a +16 kHz mono WAV and produces transcript text in the canonical diarized format +`[start][Sxx]text[end]` (e.g. `[0.48][S01]Welcome[1.66]`) via greedy decoding. +The speaker tags (`[S01]`, `[S02]`, ...) and segment start/end timestamps are +emergent generated text, not special tokens. Built for long-form, +multi-speaker audio. No translation; not a streaming model. + +See OpenMOSS's [model card](https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize) +for training data, intended use, and upstream evaluation. All of OpenMOSS's +published metrics are Chinese multi-speaker diarization CER/cpCER; LibriSpeech +test-clean is used here only as an English acceptance set. + +Licensed Apache-2.0. Ported from upstream commit +[`d7231bb`](https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize/commit/d7231bbae2587a4af278735eb765b318c4f64edd), +pinned 2026-07-12. + +## Memory and length + +MOSS keeps the whole recording in memory while it transcribes, so RAM use grows +with how long the audio is. Plan for roughly **85 MB of extra memory per minute +of audio**, on top of the model file itself — about **2.5 GB for a 30-minute +clip** and **~5 GB for an hour**. It can handle recordings up to a couple of +hours if you have the memory; anything longer is rejected with a clear error +instead of being silently cut off. If you run low on memory, split long audio +into shorter pieces. + +## Download + +| Quantization | Download | Size | WER (LibriSpeech test-clean) | +| --- | --- | ---: | ---: | +| BF16 | [MOSS-Transcribe-Diarize-BF16.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-BF16.gguf) | 1.83 GB | 2.08% | +| F16 | [MOSS-Transcribe-Diarize-F16.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-F16.gguf) | 1.83 GB | 2.07% | +| Q8_0 | [MOSS-Transcribe-Diarize-Q8_0.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-Q8_0.gguf) | 987 MB | 1.93% | +| Q6_K | [MOSS-Transcribe-Diarize-Q6_K.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-Q6_K.gguf) | 768 MB | 1.96% | +| Q5_K_M | [MOSS-Transcribe-Diarize-Q5_K_M.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-Q5_K_M.gguf) | 700 MB | 1.99% | +| Q4_K_M | [MOSS-Transcribe-Diarize-Q4_K_M.gguf](https://huggingface.co/handy-computer/MOSS-Transcribe-Diarize-gguf/resolve/main/MOSS-Transcribe-Diarize-Q4_K_M.gguf) | 617 MB | 2.59% | + +These WER values describe this dataset only, not a general quality ranking. A +quant that scores slightly better here is not necessarily better in real-world +use; dataset-specific decoding near-ties can make quantization noise help or +hurt individual utterances. + +WER measured on the full LibriSpeech `test-clean` split (2620 utterances) with +the Whisper-style English normalizer and jiwer 3.x. MOSS emits the diarized +format `[start][Sxx]text[end]`; the bracket spans are metadata and are +de-diarized to a space (for both hypothesis and reference) before scoring, +matching the author-repo reference runner. The same-manifest MOSS author-repo +reference (bf16, greedy) lands at **2.07%**, 95% bootstrap CI [1.82%, 2.40%]; +the BF16 port lands at 2.08%, within `+0.01pp` of the reference and well inside +the CI. Q4_K_M's higher 2.59% is not broad degradation but a handful of 4-bit +tail failures (6 empty outputs, 5 English->Chinese language-drift utterances, +1 timestamp-token repetition loop); prefer Q5_K_M or higher if those matter. +Dediarization is scoring-only: the runtime continues to return the raw inline +timestamps and speaker labels. + +## Quick Start + +```bash +cmake -B build +cmake --build build + +build/bin/transcribe-cli \ + -m models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-Q8_0.gguf \ + samples/jfk.wav +``` + +If your audio is not already 16 kHz mono WAV, convert it first: + +```bash +ffmpeg -i input.mp3 -ar 16000 -ac 1 output.wav +``` + +CLI flags: + +- `-l en` / `-l zh` (or omit for `auto`): English and Chinese are supported. +- No `--task` / `--target-language`: the model is ASR-only, no translation. +- Diarization tags and segment timestamps appear inline in the transcript + text; there is no toggle to suppress them. + +## Performance + +Cells are wall-clock latency (mean over 3 iterations after 1 warmup), +with speedup over realtime in parentheses. Units: `ms` below 1 s, `s` +above (2 decimal places). + +### Apple M4 Max + +| Backend | Sample | Q8_0 | Q4_K_M | +| ------- | ------------ | ------------: | ------------: | +| Metal | jfk (11.0s) | 388 ms (28.3×) | 369 ms (29.8×) | +| Metal | dots (35.3s) | 1.27 s (27.8×) | 1.17 s (30.1×) | +| CPU | jfk (11.0s) | 2.06 s (5.3×) | 2.37 s (4.6×) | +| CPU | dots (35.3s) | 5.71 s (6.2×) | 5.84 s (6.0×) | + +macOS 26.5.1, transcribe.cpp `e745720`. + +### AMD Ryzen 7 PRO 4750U + +| Backend | Sample | Q8_0 | Q4_K_M | +| ------- | ------------ | ------------: | ------------: | +| Vulkan | jfk (11.0s) | 3.88 s (2.8×) | 3.68 s (3.0×) | +| Vulkan | dots (35.3s) | 11.38 s (3.1×) | 10.68 s (3.3×) | +| CPU | jfk (11.0s) | 7.49 s (1.5×) | 7.06 s (1.6×) | +| CPU | dots (35.3s) | 21.20 s (1.7×) | 19.22 s (1.8×) | + +Fedora Linux 43, transcribe.cpp `e745720`. Vulkan device: `AMD Radeon +Graphics (RADV RENOIR)`. + +Benchmark reproduction: + +```bash +uv run scripts/bench/run.py \ + --models moss-transcribe-diarize \ + --quants q8_0,q4_k_m \ + --samples jfk,dots \ + --backends metal,cpu,vulkan \ + --iters 3 --warmup 1 \ + --name moss-transcribe-diarize-publication +``` + +## Numerical Validation + +transcribe.cpp is validated tensor-by-tensor against the MOSS author repo +(`scripts/dump_reference_moss_author.py`, `trust_remote_code`) on +`samples/jfk.wav` with the strict CPU backend. The reference runs BF16 (torch, +eager attention); the C++ path dequantizes BF16 weights to F32 and computes in +F32, so C++ is the *more* precise side and the residual gap is a constant +~1-3% relative bf16-vs-f32 drift, not a bug. The transcript compare is +`dediarized` (bracket metadata stripped to a space). Confirmed WER-neutral: on +the first 100 test-clean utterances the C++ ref-dtype WER (1.40%) is +bit-identical to the Oracle reference on the same subset (1.40%). Tolerances +are pinned in `tests/tolerances/moss.json` with a `_comment` block naming the +precision regime, the large-pre-normalization-activation maxes, and the encoder +padding-trim contract. Last validated at commit +[`3f5e15c`](https://github.com/handy-computer/transcribe.cpp/tree/3f5e15c). + +| Field | Value | +| --- | --- | +| Reference | MOSS author repo (`OpenMOSS-Team/MOSS-Transcribe-Diarize`) | +| Dump script | `scripts/dump_reference_moss_author.py` | +| Manifest | `tests/golden/moss/moss-transcribe-diarize.manifest.json` | +| Tolerances | `tests/tolerances/moss.json` | +| Command | `uv run scripts/validate.py all --family moss --variant moss-transcribe-diarize` | + +Selected tensors (observed on CPU, strict backend; see the tolerance file for +budgets and per-tensor notes): + +| Tensor | Max abs diff | Mean abs diff | Notes | +| --- | ---: | ---: | --- | +| `enc.mel.in` | `9.872e-05` | `5.150e-06` | C++ MelFrontend vs reference feature extractor | +| `enc.pos_add.out` | `5.000e-02` | `2.200e-03` | Encoder input + positional embedding | +| `enc.block.0.out` | `1.200e-01` | `4.000e-03` | First Whisper encoder block | +| `enc.block.23.out` | `3.100e+03` | `8.000e-02` | Pre-final-LN residual; ref \|max\| ~3.6e3, bf16 rel error dominates max_abs (renormalized by `enc.ln_post`) | +| `enc.ln_post.out` | `1.300e+01` | `3.000e-03` | Encoder output LayerNorm | +| `enc.merge.out` | `1.500e+00` | `3.000e-03` | 4x temporal merge | +| `enc.adaptor.out` | `4.000e-01` | `1.200e-02` | VQAdaptor decoder handoff (rel_mean ~0.96%) | +| `dec.audio_injected` | `4.000e-01` | `7.000e-03` | Audio tokens scattered into the prompt | +| `dec.block.0.out` | `7.500e-01` | `9.500e-03` | First Qwen3 decoder block | +| `dec.block.27.out` | `3.200e+02` | `2.600e-01` | Pre-final-RMSNorm residual (bf16 accumulation over 28 layers) | +| `dec.out_before_head` | `8.500e+00` | `8.000e-02` | Pre-head hidden state | +| `dec.logits_raw` | `4.800e-01` | `7.200e-02` | Prefill logits; argmax preserved (transcript exact) | +| `dec.logits_raw.gen8` | `5.000e-01` | `6.500e-02` | Greedy step 8 logits (KV-cache decode coverage) | +| `dec.token_emb` | `0.000e+00` | `0.000e+00` | Pure embedding lookup (pinned exact) | + +For the full porting writeup, see +[`docs/porting/families/moss.md`](../porting/families/moss.md). + +## Reproduction + +### Convert + +```bash +uv run --project scripts/envs/moss \ + scripts/convert-moss.py OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd +``` + +### Quantize + +```bash +uv run scripts/quantize-all.py models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf +``` + +### Validate + +```bash +uv run scripts/validate.py all --family moss --variant moss-transcribe-diarize +``` + +### Score WER + +```bash +PRESET=BF16 +uv run scripts/wer/run.py \ + --model models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-${PRESET}.gguf \ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \ + --out reports/wer/MOSS-Transcribe-Diarize-${PRESET}.librispeech-test-clean.jsonl +uv run scripts/wer/score.py \ + reports/wer/MOSS-Transcribe-Diarize-${PRESET}.librispeech-test-clean.jsonl \ + --dediarize +``` + +### Score WER against the MOSS author-repo reference + +```bash +uv run --project scripts/envs/moss \ + scripts/wer/run_reference_moss_author.py \ + --model OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd \ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \ + --out reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl +uv run scripts/wer/score.py \ + reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl \ + --dediarize +``` diff --git a/docs/porting/families/moss.md b/docs/porting/families/moss.md new file mode 100644 index 00000000..7089ff2d --- /dev/null +++ b/docs/porting/families/moss.md @@ -0,0 +1,227 @@ +# MOSS-Transcribe-Diarize + +Status: supported + +## Identity + +- Family key: `moss` +- Upstream architecture string: `MossTranscribeDiarizeForConditionalGeneration` (`model_type: moss_transcribe_diarize`) +- Hugging Face repo: `OpenMOSS-Team/MOSS-Transcribe-Diarize` +- Hugging Face revision: `d7231bbae2587a4af278735eb765b318c4f64edd` +- License: Apache-2.0 +- Variants: `moss-transcribe-diarize` (0.9B; Whisper-Medium encoder + VQAdaptor + Qwen3-0.6B decoder) + +Architecture pattern: **audio-llm**. log-mel `input_features` → `WhisperEncoder` +(24 layers, d_model 1024, gelu, LayerNorm) → 4× temporal merge (1024 → 4096) → +`VQAdaptor` MLP (Linear 4096→1024, SiLU, Linear 1024→1024, LayerNorm w/ bias) → +`masked_scatter` into Qwen3-0.6B text embeddings at `<|audio_pad|>` (id 151671) +positions → Qwen3 decoder (28 layers, hidden 1024, silu, RMSNorm, GQA 16/8 heads, +head_dim 128, rope_theta 1e6, tie_word_embeddings) → `lm_head`. + +Output is emergent plain text in the canonical format `[start][Sxx]text[end]` +(e.g. `[0.48][S01]Welcome[1.66]`); diarization speaker tags and segment +timestamps are generated text, not special tokens. + +## References + +- Canonical reference: `author_repo_moss` — OpenMOSS custom `trust_remote_code` + files in the HF repo (`modeling_moss_transcribe_diarize.py`, + `configuration_moss_transcribe_diarize.py`, + `processing_moss_transcribe_diarize.py`) plus the OpenMOSS/MOSS-Transcribe-Diarize + GitHub inference harness (`build_transcription_messages`, `generate_transcription`, + audio/video loading, transcript parsing). Not in upstream Transformers. +- Instrumented reference: same custom code (single source of truth; no framework fork). +- Cross-check references: `Qwen3Model` and `WhisperEncoder` upstream Transformers + implementations that the custom code composes. + +## Commands + +Reference WER run: + +```bash +uv run --project scripts/envs/moss \ + scripts/wer/run_reference_moss_author.py \ + --model OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd \ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \ + --out reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl +uv run scripts/wer/score.py \ + reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl \ + --dediarize +``` + +Reference dumps: + +```bash +uv run --project scripts/envs/moss \ + scripts/dump_reference_moss_author.py decode \ + --model OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd \ + --audio samples/jfk.wav \ + --out build/validate/moss/moss-transcribe-diarize/jfk/decode/ref +``` + +Conversion: + +```bash +uv run --project scripts/envs/moss \ + scripts/convert-moss.py OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd +``` + +Validation: + +```bash +uv run scripts/validate.py all --family moss --variant moss-transcribe-diarize +``` + +Benchmarks: + +```bash +uv run scripts/bench/run.py \ + --models moss-transcribe-diarize \ + --quants q8_0,q4_k_m \ + --samples jfk,dots \ + --backends metal,cpu,vulkan \ + --iters 3 --warmup 1 \ + --name moss-transcribe-diarize-publication +``` + +## Capability Validation + +| Capability | Mode | Command / test | Expected observable | Target | Status | +|------------|------|----------------|---------------------|--------|--------| +| Transcribe | auto / no language hint (en) | `build/bin/transcribe-cli -m models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf samples/jfk.wav` | non-empty plausible English transcript | MUST PASS | **PASS** — `[0.28][S01] And so, my fellow Americans, ask not what your country can do for you, ask what you can do for your country.[10.59]` | +| Transcribe | auto / no language hint (zh) | `build/bin/transcribe-cli -m models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf samples/zh.wav` | non-empty plausible Chinese transcript | MUST PASS | **PASS** — `[0.65][S01]开放时间早上九点至下午五点[5.22]` | +| Transcribe | explicit language hint | `build/bin/transcribe-cli -m ... --language en samples/jfk.wav` | non-empty plausible transcript | OUT OF SCOPE — no first-class language token; language is inferred from audio and steered only by prompt text. Back in scope if the runtime exposes a prompt/language selector. | **SKIP — not exposed by runtime** (`--language` is accepted but ignored; the prompt is the fixed baked instruction) | +| Speaker diarization | default diarize prompt | `build/bin/transcribe-cli -m ... samples/jfk.wav` | `[Sxx]` speaker tags present in the raw transcript | OUT OF SCOPE — diarized `[Sxx]` output is produced correctly in the raw transcript (rides the same output format the Segment-timestamps row gates), but there is no runtime API surface to expose diarization as a distinct capability; a dedicated API surface is out of scope for this PR. Back in scope when that API lands. | **ACCEPTED GAP — `[S01]` tags present in the raw transcript, no distinct diarization API** (unblocked when a diarization API surface lands) | +| Segment timestamps | default diarize prompt | `build/bin/transcribe-cli -m ... samples/jfk.wav` | output contains `[start]...[end]` numeric timestamps | MUST PASS | **PASS** — output carries inline `[0.28]…[10.59]` segment timestamps | +| Word timestamps | — | — | word-granularity timestamps | OUT OF SCOPE — model emits segment-level `[start][end]` only; no word alignment. | **SKIP — not exposed by runtime** | +| Long-form audio | >30s multi-chunk | `build/bin/transcribe-cli -m ... samples/whole-earth.wav` | full transcript across 30s chunk boundaries | MUST PASS | **PASS** — 84s clip transcribed in full across 3 chunks to EOS (`…Thank you all very much.[83.47]`) | +| Promptable / hotwords | custom prompt append | `` | hotword-biased transcript | OUT OF SCOPE — custom prompt/hotword injection is not part of the core transcribe path. Back in scope if a prompt-override flag ships. | **SKIP — not exposed by runtime** | +| Acoustic event annotation | promptable | `` | event annotations in output | OUT OF SCOPE — optional promptable feature, not the core transcribe path. | **SKIP — not exposed by runtime** | +| Translate | — | — | English output on non-English audio | OUT OF SCOPE — transcription-only model; no translation capability. | **SKIP — not exposed by runtime** | +| Language detection | — | — | detected language surfaced | OUT OF SCOPE — no detection branch/token; language is not surfaced by the runtime. | **SKIP — not exposed by runtime** | +| Streaming | — | — | streaming path | OUT OF SCOPE — offline model; `capabilities.streaming: false`. | **SKIP — not exposed by runtime** | +| Batch (offline) | run_batch vs serial | `uv run scripts/batch_parity.py --model models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf --list --batch-sizes 2,4,8 --backend cpu` | byte-identical hypotheses + CPU tensor parity | MUST PASS | **PASS** — text parity byte-identical at batch 2/4/8 vs serial (12 utts, CPU); golden fixture `tests/golden/batch/moss-transcribe-diarize.cpu.json`. Encoder tensor parity is structural (see Notes). | + +## Notes + +- **Ref-dtype WER (LibriSpeech test-clean, 2620 utts).** C++ BF16 GGUF: + **2.08%**. Oracle reference (our own BF16 run): **2.07%**. The +0.01pp + difference is at the strict `Oracle + 0.01pp` gate. Cause: 27/2620 utts + (1.03%) flip a single word under + bf16(ref)-vs-f32(cpp) precision (homophones / proper nouns: parquet↔parakeet, + berksen↔bergson, ...), several arguably more correct in the C++ output. CIs + overlap ~99% ([1.84,2.41] vs [1.82,2.40]). Root-caused, not assumed: re-running + the reference in **F32** on all 27 flip utts resolves 26/27 to exactly the C++ + output (C++(f32) == reference-F32), i.e. the gap is the reference's own BF16 + imprecision, not a port bug. At each flip the two candidate tokens are a + near-tie whose logit gap (~0.05-0.25, e.g. an exact 0.000 bf16 tie for + save/saved) sits inside the measured `dec.logits` bf16 noise band; several C++ + picks are the more correct word (parakeet, bergson, specialty, neighbour). The + 0.01pp gate is below this model's bf16 precision floor. +- **Batch (offline) design.** `run_batch` batches the *decoder* (batched KV + cache + `block_prefill_batched` + `run_batched_step_loop`) — the dominant cost + of transcription. The *encoder* runs per-utterance (`encode_one`) in both the + serial and batched paths, so the per-utterance encoder output is bit-exact by + construction. That is why `scripts/batch_tensor_parity.py` (which compares a + *fused* batched-encoder dump `.b{i}` against the single-shot, as + parakeet/qwen3_asr produce) does not apply here — MOSS has no fused batched + encoder. Batched correctness is gated by `batch_parity.py`: byte-identical + hypotheses at batch 2/4/8 vs serial, which exercises the real batched decoder. +- Acceptance dataset (user-signed): **LibriSpeech test-clean** (English) as the + ref-dtype gate, **plus a Chinese diarization set** (Stage 2 picks the specific + corpus, e.g. AISHELL-4 / Alimeeting) as an on-target companion. The model + supports English, so LibriSpeech is valid, but it is off the model's primary + use case (Chinese/English long-form multi-speaker diarization). All publisher + benchmarks are Chinese diarization CER/cpCER (AISHELL-4, Alimeeting, Podcast, + Movies); there is no published English number. Downstream gates use the + measured Oracle reference baseline, not a publisher score. **Not completed in + this PR:** the Chinese companion set still needs a pinned corpus/manifest and + cpCER-style scoring infrastructure; this remains a post-merge follow-up. +- WER scoring must de-diarize/de-timestamp hypotheses: strip `[Sxx]` speaker + tags and `[start]`/`[end]` numeric timestamps from generated text before + scoring against LibriSpeech references. Pass `--dediarize` to + `scripts/wer/score.py`; runtime output remains raw. +- The default instruction prompt is Chinese and is used for English audio too; + there is no language/task token (`has_language_tokens: false`). +- Prompt/audio-span construction is non-trivial: the processor interleaves + literal second-count digit tokens into the `<|audio_pad|>` span. The + authoritative cadence is **every 5s** (`processor_config.json`: + `audio_tokens_per_second=12.5`, `time_marker_every_seconds=5`, + `enable_time_marker=true`) — the constructor default of 2s is overridden by + the saved processor config. These are emitted as `stt.moss.*` KV. See + `known_risks` in the intake. +- See `reports/porting/moss/moss-transcribe-diarize/intake.json`, the pinned + [upstream model revision](https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize/tree/d7231bbae2587a4af278735eb765b318c4f64edd), + and the local reference dumper for the source contract. + +## Quant matrix + +- Full derived matrix produced and CLI-smoked (jfk): F16 (1.83 GB), Q8_0 + (0.99 GB), Q6_K (0.77 GB), Q5_K_M (0.70 GB), Q4_K_M (0.62 GB). Artifacts are + staged in `handy-computer/moss-transcribe-diarize-gguf`; the repo is planned + to become public after this PR merges. +- WER, LibriSpeech test-clean, batch 1, CUDA/L4 (Modal): BF16 **2.08%**, + F16 **2.07%**, Q8_0 **1.93%**, Q6_K **1.96%**, Q5_K_M **1.99%**, Q4_K_M + **2.59%** (2.37% excluding one runaway; **2.16%** on Metal). These dataset + scores are not a general quality ranking. The Q8/Q6/Q5 tiers + scoring *below* BF16 is not a quality gain: it is one 31.6s utterance + (`4507-16021-0026`) where BF16/F16 *and the torch reference itself* take an + immediate-EOS first-token near-tie and emit empty, while quant rounding noise + perturbs that tie and recovers the 65 words. Same bf16-near-tie mechanism as the + ref-dtype flips, here dropping/recovering a whole utterance. +- **Q4_K_M repetition runaway (shipped as-is).** One utterance + (`237-134493-0017`, 1/2620 = 0.04%) degenerates into a repetition loop under + Q4_K_M **on CUDA only** — it emits a garbled `0:01,` token to the generation cap + (decode ~8.6s vs ~1s) and returns `TRANSCRIBE_ERR_OUTPUT_TRUNCATED`. **Metal + decodes the same utterance correctly** (0 runaways, 2.16%). This is bounded 4-bit + near-tie degeneration on a 0.6B decoder, backend-numerics-sensitive, not a + corrupted tensor; the runtime caps it (no hang). Decision: keep Q4_K_M in the + matrix — one bounded, backend-specific runaway is not disqualifying and the + smallest tier stays available. A repetition/no-repeat decoding guard is a + possible future mitigation for the most aggressive quants (separate decoding- + policy discussion, out of Stage 5 scope). +- **Harness fix (`examples/cli/main.cpp`, batch mode).** The Q4_K_M runaway first + surfaced as a *sweep abort*: the batch CLI returned `EXIT_FAILURE` whenever any + single utterance failed, so run.py discarded all 2620 hyps over one truncation. + Fixed to collect per-utterance errors instead: `OUTPUT_TRUNCATED` is + result-bearing (the preserved partial hyp is emitted as `text` and scored, still + tagged), a new `truncated` count is reported, and truncation alone does not make + batch mode fail. Hard per-utterance failures still produce a non-zero exit so + automation cannot accept a partial sweep. Family-agnostic (moss/granite return + the truncation code); MOSS is the first port whose most-aggressive quant tripped + it. Validated on both backends: the truncation-only sweep completes at exit 0. + +## Stage 3 conversion — tensor-mapping decisions + +- **Encoder names reuse the `whisper` family**, decoder names reuse the + `qwen3_asr` text LM. MOSS's `model.whisper_encoder.*` is a stock HF + `WhisperEncoder`, so it maps to `enc.conv.{0,1}`, `enc.pos_emb`, + `enc.final_norm`, `enc.blocks.N.{norm_attn,attn.{q,k,v,out},norm_ffn,ffn.{fc1,fc2}}` + (q/v/out have bias, k does not). The Qwen3 LM maps to + `dec.token_embd`, `dec.output_norm`, + `dec.blocks.N.{norm_attn,norm_ffn,attn.{q,k,v,o,q_norm,k_norm},ffn.{gate,up,down}}`. + This lets Stage 4 share the whisper encoder graph and a Qwen3 decoder graph. +- **VQAdaptor** (`model.vq_adaptor.layers.{0,2,3}`) maps to `adaptor.fc1` + (Linear 4096→1024), `adaptor.fc2` (Linear 1024→1024), `adaptor.norm_out` + (final LayerNorm). `norm_out` is deliberately named so the `"norm_"` rule in + `reference_dtype_for` keeps the LayerNorm scale at F32. +- **Tied head:** `tie_word_embeddings=true` and `lm_head.weight` is absent from + the state dict; the loader reuses `dec.token_embd.weight` for the output + projection. No SKIP list needed. +- **Vocab padded to 151936.** The tokenizer names 151672 tokens but the + embedding/logit width is `text_config.vocab_size=151936`; the token table is + padded with `<|unused_N|>` so its length matches the logits. Intake + + manifest `vocab_size` corrected from 151672 to 151936 accordingly. +- **Conv1d kernels → F16.** Reference dtype is BF16 but the loader has no BF16 + conv kernel, so `enc.conv.{0,1}.weight` are stored F16 per + `reference_dtype_for`. No `policy.cpp` change was needed — every MOSS tensor + name hits an existing bucket rule (quant-policy sync passes 5/5). +- **Frontend buffers baked in:** `frontend.mel_filterbank` (slaney) and + `frontend.window` (periodic Hann) are written as F32 tensors so the C++ + MelFrontend is bit-identical. +- GGUF path is `models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf` + (upstream-slug convention, matches `Qwen3-ASR-0.6B/` etc.); `stt.variant` is + the lowercase `moss-transcribe-diarize`. diff --git a/docs/tools/wer.md b/docs/tools/wer.md index 1410aa54..ae5728a9 100644 --- a/docs/tools/wer.md +++ b/docs/tools/wer.md @@ -121,6 +121,16 @@ alongside the input: `wer_ci_lo` / `wer_ci_hi` are a 95% bootstrap CI (1000 resamples, seed=42 — deterministic). +Diarizing models may emit inline timestamps and speaker labels. Remove those +spans explicitly when the evaluation contract treats them as metadata: + +```bash +uv run scripts/wer/score.py reports/wer/.jsonl --dediarize +``` + +This is opt-in because bracketed annotations such as `[laughter]` or `` +may be meaningful scoring content in other datasets. + ## `compare.py` — delta table ```bash diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index c407f98b..93c1c889 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -734,8 +734,9 @@ int main(int argc, char ** argv) { std::fflush(stdout); } - int n_ok = 0; - int n_fail = 0; + int n_ok = 0; + int n_truncated = 0; // result-bearing: hit the generation cap, partial hyp emitted + int n_fail = 0; // no usable result (wav load / backend / unsupported / whole-batch) // Offline batched path: group up to batch_size utterances into one // transcribe_run_batch call. Mutually exclusive with the streaming @@ -802,15 +803,25 @@ int main(int argc, char ** argv) { } for (size_t k = 0; k < src_index.size(); ++k) { - const std::string & wav = wav_paths[src_index[k]]; - const transcribe_status ust = transcribe_batch_status(ctx, static_cast(k)); - const char * text = ""; - if (ust == TRANSCRIBE_OK) { + const std::string & wav = wav_paths[src_index[k]]; + const transcribe_status ust = transcribe_batch_status(ctx, static_cast(k)); + // OUTPUT_TRUNCATED is result-bearing: the partial transcript is + // preserved and readable via transcribe_batch_full_text (see + // transcribe.h). Emit it as the hyp so downstream tooling scores + // the partial rather than an empty string; the error field below + // still tags it so the truncation stays visible. + const bool result_present = ust == TRANSCRIBE_OK || ust == TRANSCRIBE_ERR_OUTPUT_TRUNCATED; + const char * text = ""; + if (result_present) { const char * t = transcribe_batch_full_text(ctx, static_cast(k)); if (t && *t) { text = t; } + } + if (ust == TRANSCRIBE_OK) { ++n_ok; + } else if (ust == TRANSCRIBE_ERR_OUTPUT_TRUNCATED) { + ++n_truncated; } else { ++n_fail; } @@ -838,10 +849,12 @@ int main(int argc, char ** argv) { (double) tm.decode_ms, err_field.c_str()); } else { std::printf("[%zu/%zu] %s", src_index[k] + 1, total, wav.c_str()); - if (ust != TRANSCRIBE_OK) { - std::printf(" ERROR: %s\n", transcribe_status_string(ust)); - } else { + if (ust == TRANSCRIBE_OK) { std::printf("\n text: %s\n", text); + } else if (ust == TRANSCRIBE_ERR_OUTPUT_TRUNCATED) { + std::printf(" (truncated)\n text: %s\n", text); + } else { + std::printf(" ERROR: %s\n", transcribe_status_string(ust)); } } std::fflush(stdout); @@ -924,13 +937,23 @@ int main(int argc, char ** argv) { run_st = transcribe_run(ctx, pcm.data(), static_cast(pcm.size()), &rp); } - const char * text = ""; - if (run_st == TRANSCRIBE_OK) { + // OUTPUT_TRUNCATED is result-bearing: the partial transcript is + // preserved and readable via transcribe_full_text (see transcribe.h). + // Emit it as the hyp so downstream tooling scores the partial rather + // than an empty string; the error field below still tags it so the + // truncation stays visible. + const bool result_present = run_st == TRANSCRIBE_OK || run_st == TRANSCRIBE_ERR_OUTPUT_TRUNCATED; + const char * text = ""; + if (result_present) { const char * t = transcribe_full_text(ctx); if (t && *t) { text = t; } + } + if (run_st == TRANSCRIBE_OK) { ++n_ok; + } else if (run_st == TRANSCRIBE_ERR_OUTPUT_TRUNCATED) { + ++n_truncated; } else { ++n_fail; } @@ -962,10 +985,12 @@ int main(int argc, char ** argv) { (double) tm.decode_ms, err_field.c_str()); } else { std::printf("[%zu/%zu] %s", i + 1, wav_paths.size(), wav.c_str()); - if (run_st != TRANSCRIBE_OK) { - std::printf(" ERROR: %s\n", transcribe_status_string(run_st)); - } else { + if (run_st == TRANSCRIBE_OK) { std::printf("\n text: %s\n", text); + } else if (run_st == TRANSCRIBE_ERR_OUTPUT_TRUNCATED) { + std::printf(" (truncated)\n text: %s\n", text); + } else { + std::printf(" ERROR: %s\n", transcribe_status_string(run_st)); } } std::fflush(stdout); @@ -973,11 +998,14 @@ int main(int argc, char ** argv) { } if (!args.batch_jsonl) { - std::fprintf(stderr, "batch: %d ok, %d failed out of %zu\n", n_ok, n_fail, wav_paths.size()); + std::fprintf(stderr, "batch: %d ok, %d truncated, %d failed out of %zu\n", n_ok, n_truncated, n_fail, + wav_paths.size()); } transcribe_session_free(ctx); transcribe_model_free(model); + // OUTPUT_TRUNCATED is result-bearing and does not fail the batch, but + // hard per-utterance failures must remain visible to automation. return n_fail > 0 ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/reports/porting/moss/forward-map.md b/reports/porting/moss/forward-map.md new file mode 100644 index 00000000..eb91cc01 --- /dev/null +++ b/reports/porting/moss/forward-map.md @@ -0,0 +1,86 @@ +# Forward map - moss + +Reference: OpenMOSS custom `trust_remote_code` files (`modeling_moss_transcribe_diarize.py`, +`processing_moss_transcribe_diarize.py`) in `OpenMOSS-Team/MOSS-Transcribe-Diarize` +@ `d7231bbae2587a4af278735eb765b318c4f64edd`. +Closest in-tree analogs: `src/arch/whisper/` (encoder) + `src/arch/qwen3_asr/` +(Qwen3 decoder, audio-token injection, batch/step drivers, `src/causal_lm/`). + +audio-llm: log-mel -> WhisperEncoder (24L) -> 4x time merge (1024->4096) -> +VQAdaptor (4096->1024) -> masked_scatter into Qwen3-0.6B embeds at `<|audio_pad|>` +(151671) positions -> Qwen3 decoder (28L) -> tied lm_head. Diarization/timestamps +are emergent plain text (`[start][Sxx]text[end]`), not tokens. + +## Frontend + +| Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog | +|-------|--------------------|--------------|-------------|--------------------|----------------| +| Chunk + pad audio to 30s | `processing._chunk_audio` (n_samples=480000; last chunk zero-padded to 30s) | list of [480000] chunks + per-chunk `token_len=(n-1)//1280+1` | (host) | host loop; one mel+encoder pass per chunk | qwen3_asr host chunking | +| Log-mel (WhisperFeatureExtractor) | `feature_extractor` (80 mel, n_fft 400, hop 160, hann_periodic, center/reflect, slaney, per-utterance log norm) | [80, 3000] per chunk | `enc.mel.in` | `transcribe::MelFrontend` w/ baked `frontend.mel_filterbank` + `frontend.window` | whisper / qwen3_asr MelFrontend | + +## Encoder + +| Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog | +|-------|--------------------|--------------|-------------|--------------------|----------------| +| Conv1 (k3 s1 p1) + GELU-erf | `WhisperEncoder.conv1` | [1024, 3000] | — | `conf::conv_1d_f32` (F16 kernel, F32 in), `ggml_gelu_erf` | whisper `encoder.cpp` | +| Conv2 (k3 s2 p1) + GELU-erf | `WhisperEncoder.conv2` | [1024, 1500] | — | same, stride 2 | whisper | +| + learned pos emb | `embed_positions.weight` | [1024, 1500] | `enc.pos_add.out` | `ggml_add` (F32 `enc.pos_emb.weight`); no scale_embedding | whisper | +| 24x pre-LN block (LayerNorm, MHA gelu FFN; q/v/out have bias, k none) | `WhisperEncoderLayer` | [1024, 1500] | `enc.block.0.out`, `enc.block.23.out` | `conf::layer_norm` + `mha_encoder` (bidir, no mask) + gelu-erf FFN | whisper `build_block` | +| Final LayerNorm | `WhisperEncoder.layer_norm` | [1024, 1500] | `enc.ln_post.out` | `conf::layer_norm` | whisper | +| Per-chunk trim to `token_len*4` cols, concat chunks | `get_audio_features` (`whisper_features[:, :token_len*4]`) | [1024, sum(token_len)*4] | — | host trims cols before merge; single-chunk jfk -> [1024, 552] | new (moss) | +| 4x time merge | `time_merge`: `[:, :T_trim].reshape(B, T//4, D*4)` | [4096, T_enc] | `enc.merge.out` | `ggml_cont` then `ggml_reshape_2d(1024,T)->(4096,T/4)` (4 consec frames -> 4096) | new (moss) | +| VQAdaptor: fc1(4096->1024)+bias, SiLU, fc2(1024->1024)+bias, LayerNorm(eps 1e-6, bias) | `VQAdaptor.layers` | [1024, T_enc] | `enc.adaptor.out` | `mul_mat`+bias, `ggml_silu`, `mul_mat`+bias, `conf::layer_norm` (`adaptor.norm_out`) | new (moss) | + +## Decoder + +Qwen3-0.6B: 28 layers, GQA 16/8, head_dim 128, per-head q/k RMSNorm, NeoX RoPE +theta 1e6, SwiGLU (packed gate+up), RMSNorm eps ~1e-6, tied head. Identical block +math to `qwen3_asr` via `src/causal_lm/` (`BlockView`/`block_prefill`/`block_step`). + +| Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog | +|-------|--------------------|--------------|-------------|--------------------|----------------| +| Prompt build (chat wrapper + audio span + time markers) | `processor.__call__` / `_audio_span_ids` | [T_prompt] i32 (jfk: 227) | — | prefix ids + audio-span (host int math) + suffix ids; baked `stt.moss.prompt_*_tokens` / `digit_tokens` KV | qwen3_asr `build_prompt_tokens` | +| Token embed (all positions, pre-injection) | `Qwen3Model.embed_tokens` | [1024, T_prompt] | `dec.token_emb` | `ggml_get_rows(dec.token_embd, input_ids)` | qwen3_asr | +| Audio injection (masked_scatter at audio_pad) | `inject_audio_features` | [1024, T_prompt] | `dec.audio_injected` | elementwise blend `x*keep_mask + audio_dense` (audio positions non-contiguous due to markers) | qwen3_asr batched prefill blend | +| 28x Qwen3 block (prefill, causal) | `Qwen3Model.layers` | [1024, T_prompt] | `dec.block.0.out`, `dec.block.27.out` | `causal_lm::block_prefill` | qwen3_asr `decoder.cpp` | +| Final RMSNorm | `Qwen3Model.norm` | [1024, T_prompt] | `dec.out_before_head` | `ggml_rms_norm * dec.output_norm` | qwen3_asr | +| Tied lm_head (last position) | `lm_head` (tied to embed) | [151936] | `dec.logits_raw` | `ggml_mul_mat(dec.token_embd, last_x)` | qwen3_asr | + +## Generation / KV Path + +| Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog | +|-------|--------------------|--------------|-------------|--------------------|----------------| +| Prefill writes KV [0,T_prompt) | `Qwen3Model` w/ cache | — | — | `block_prefill` into `causal_lm::KvCache` | qwen3_asr | +| Greedy step (argmax, one token) | `generate(do_sample=False)` | [1] i32/step | `dec.logits_raw.gen` (N>=8, to add) | static `build_step_graph` reused per step | qwen3_asr `build_step_graph` | +| Stop on EOS 151645 / max_new | GenerationMixin | — | — | host loop, de-diarize before scoring | qwen3_asr run loop | +| Batched offline (B utts) | — | — | — | `kv_init_batched` + `block_prefill_batched` + `run_batched_step_loop` | qwen3_asr run_batch | + +## Capabilities And Language Controls + +| Capability | Reference behavior | C++ API behavior | Family-doc Capability Validation row | +|------------|--------------------|------------------|--------------------------------------| +| Transcribe en/zh (auto) | fixed Chinese diarize prompt for all audio | raw text -> de-diarize | Transcribe auto (en/zh) MUST PASS | +| Segment timestamps | emergent `[start][end]` text | present in raw transcript | Segment timestamps MUST PASS | +| Long-form >30s | 30s chunk + concat before merge | host chunk loop | Long-form MUST PASS | +| Diarization | emergent `[Sxx]` text | in raw transcript; no distinct API | OUT OF SCOPE (no API surface) | +| Explicit language / prompt override | prompt-text only, no token | not exposed (single baked prompt) | OUT OF SCOPE | +| Batch (offline) | — | `run_batch` parallel path | Batch MUST PASS | + +## Deviations From Closest Analog + +- Encoder is whisper (conv stem + learned PE + gelu LayerNorm blocks), NOT + qwen3_asr's conv2d-subsample encoder. Reuse whisper's `mha_encoder` shape. +- Audio injection is a **blend** (`x*keep_mask + audio_dense`), not qwen3_asr's + single-shot 3-way concat: MOSS audio-pad positions are non-contiguous because + the processor interleaves time-marker digit tokens into the span. The blend is + exactly qwen3_asr's *batched* prefill injection, reused for single prefill too. +- Prompt: fixed Chinese diarize instruction with a `system` block and + `<|audio_start|>/<|audio_end|>` wrapper. Prefix/suffix token ids + digit ids are + baked into the GGUF by the converter (from the reference tokenizer); C++ only + reproduces the deterministic `_audio_span_ids` integer math. No C++ Chinese BPE, + no Jinja rendering (language/prompt override is OUT OF SCOPE for this port). +- Merge is a pure `ggml_reshape` (row-major group-of-4), no learned op. + +## Variant Notes + +- `moss-transcribe-diarize`: family baseline (0.9B; Whisper-Medium enc + Qwen3-0.6B dec). diff --git a/reports/porting/moss/moss-transcribe-diarize/intake.json b/reports/porting/moss/moss-transcribe-diarize/intake.json new file mode 100644 index 00000000..15f86279 --- /dev/null +++ b/reports/porting/moss/moss-transcribe-diarize/intake.json @@ -0,0 +1,164 @@ +{ + "schema_version": "transcribe-intake-v1", + "family": "moss", + "hf_repo": "OpenMOSS-Team/MOSS-Transcribe-Diarize", + "hf_revision": "d7231bbae2587a4af278735eb765b318c4f64edd", + "sources": { + "config": { + "kind": "hf_file", + "path": "config.json", + "status": "found" + }, + "preprocessor": { + "kind": "hf_file", + "path": "preprocessor_config.json", + "status": "found" + }, + "tokenizer_config": { + "kind": "hf_file", + "path": "tokenizer_config.json", + "status": "found" + }, + "tokenizer_json": { + "kind": "hf_file", + "path": "tokenizer.json", + "status": "found" + }, + "generation_config": { + "kind": "hf_file", + "path": "generation_config.json", + "status": "found" + }, + "safetensors_metadata": { + "kind": "hf_api", + "path": "HfApi.get_safetensors_metadata", + "status": "found", + "detail": "header-only floating dtype distribution; no tensor payloads downloaded" + } + }, + "variants": [ + { + "name": "moss-transcribe-diarize", + "memory_gb": null, + "files": [] + } + ], + "config": { + "architecture_candidates": [ + "encoder-decoder" + ], + "key_fields": { + "architectures": [ + "MossTranscribeDiarizeForConditionalGeneration" + ], + "model_type": "moss_transcribe_diarize" + } + }, + "dtype": { + "expected": "bfloat16", + "source": "config", + "evidence": "config dtype bfloat16", + "details": { + "config_declared": "bfloat16", + "header_distribution": { + "BF16": 683 + } + } + }, + "frontend": { + "sample_rate": 16000, + "n_mels": 80, + "hop_length": 160, + "fft_size": 400, + "window": "hann_periodic", + "normalization": "per_utterance", + "preemphasis": null, + "dither": 0.0, + "center": true, + "padding_mode": "reflect", + "mel_filterbank_norm": "slaney" + }, + "tokenizer": { + "type": "bpe", + "vocab_size": 151936, + "special_tokens": { + "bos": 151643, + "eos": 151645, + "pad": 151643 + }, + "has_language_tokens": false, + "vocab_sha256": "36c3749fd3c87e2402f612459b45ae4e6ce5488a3ecde47a8a3ea3ed3d5dda5a" + }, + "capabilities": { + "languages": [ + "en", + "zh" + ], + "language_detection": false, + "translation": false, + "timestamps": [ + "segment" + ], + "streaming": false, + "speaker_diarization": true + }, + "upstream_benchmarks": [ + { + "dataset": "LibriSpeech test-clean", + "language": "en", + "metric": "wer", + "score": null, + "source": "model card", + "notes": "Publisher reports no English LibriSpeech number. This is a Chinese/English long-form multi-speaker diarization model; all published metrics are Chinese diarization CER/cpCER. LibriSpeech is used as the default English acceptance set only; downstream gates use the measured Oracle reference baseline, not a publisher score." + }, + { + "dataset": "AISHELL-4", + "language": "zh", + "metric": "cer", + "score": 14.84, + "score_unit": "percent", + "source": "model card", + "notes": "MOSS-Transcribe-Diarize 0.9B (non-Pro). cpCER 15.83, Delta-cp 0.99. Multi-speaker meeting corpus." + }, + { + "dataset": "Alimeeting", + "language": "zh", + "metric": "cer", + "score": 24.86, + "score_unit": "percent", + "source": "model card", + "notes": "MOSS-Transcribe-Diarize 0.9B (non-Pro). cpCER 22.17, Delta-cp -2.69." + }, + { + "dataset": "Podcast", + "language": "zh", + "metric": "cer", + "score": 5.97, + "score_unit": "percent", + "source": "model card", + "notes": "MOSS-Transcribe-Diarize 0.9B (non-Pro). cpCER 7.37, Delta-cp 1.40." + }, + { + "dataset": "Movies", + "language": "zh", + "metric": "cer", + "score": 6.36, + "score_unit": "percent", + "source": "model card", + "notes": "MOSS-Transcribe-Diarize 0.9B (non-Pro). cpCER 12.76, Delta-cp 6.40." + } + ], + "reference_framework": "author_repo_moss", + "reference_rationale": "The model is not part of upstream Transformers: it loads only via trust_remote_code from OpenMOSS's custom modeling_moss_transcribe_diarize.py, configuration, and processing_moss_transcribe_diarize.py files, and the OpenMOSS/MOSS-Transcribe-Diarize GitHub repo supplies the reference inference harness (build_transcription_messages, generate_transcription, audio/video loading, transcript parsing). Those custom files plus the GitHub harness are the sole source of truth for the forward pass, the audio-placeholder / time-marker prompt construction, and the 30s chunking. No NeMo path exists. Stock Transformers cannot load the model without the remote code, so the reference is the author custom code rather than the transformers library itself.", + "architecture_pattern": "audio-llm", + "known_risks": [ + "Composite three-part audio-LLM with heterogeneous norms and activations: WhisperEncoder (24 layers, gelu, LayerNorm) -> 4x temporal merge (1024 -> 4096) -> VQAdaptor MLP (Linear 4096->1024, SiLU, Linear 1024->1024, LayerNorm with bias) -> Qwen3-0.6B decoder (28 layers, silu, RMSNorm). Mixed LayerNorm-vs-RMSNorm and gelu-vs-silu across sub-blocks; each must be wired to the right normalization/activation.", + "Time-marker digit injection into the audio placeholder span. The processor does NOT emit a uniform run of <|audio_pad|> (id 151671) tokens: with enable_time_marker=True, audio_tokens_per_second=12.5, time_marker_every_seconds=5 (authoritative value from processor_config.json; the constructor default of 2 is overridden), it interleaves literal second-count digit tokens (e.g. '5','10','15', ...) among the audio placeholders at fixed positions. The C++ prompt builder must reproduce this interleaving exactly or timestamp emission and the masked_scatter feature count both break.", + "Audio token-length formula couples frontend hop, Whisper conv stride (2), and merge size (4): tokens_per_chunk = (num_samples - 1) // (hop_length * 2 * merge_size) + 1 = (n - 1) // 1280 + 1. masked_scatter requires the number of audio placeholders to exactly equal the number of adaptor output features; any off-by-one here is a hard runtime error, not silent drift.", + "Long-form handling is chunk-based, not a single encode. Audio is split into 480000-sample (30s) chunks, each padded to 30s and encoded by Whisper independently; per-chunk features are trimmed to token_len*4 then concatenated per-audio before the 4x merge and adaptor. Chunk-boundary trimming and per-audio concatenation are core to the long-form capability and easy to get wrong.", + "Diarization and timestamps are emergent plain-text output in the canonical format [start][Sxx]text[end] (e.g. [0.48][S01]Welcome[1.66]), not special tokens. There is no structural token boundary; WER/CER scoring must parse and strip speaker labels and numeric timestamps from generated text.", + "tie_word_embeddings=true: lm_head.weight is tied to model.language_model.embed_tokens.weight. The converter must emit a single shared tensor and not duplicate or diverge the two.", + "Language selection is prompt-text-only; has_language_tokens=false and there is no language/task token or detection branch. The default instruction prompt is Chinese and is used for English audio as well. English acceptance (LibriSpeech) therefore runs the model with a Chinese timestamp+diarize instruction, and the single-speaker English hypotheses must be de-diarized/de-timestamped before scoring." + ], + "intake_gaps": [] +} diff --git a/scripts/convert-moss.py b/scripts/convert-moss.py new file mode 100644 index 00000000..bebae8eb --- /dev/null +++ b/scripts/convert-moss.py @@ -0,0 +1,705 @@ +#!/usr/bin/env python3 +""" +convert-moss.py - convert a MOSS-Transcribe-Diarize HuggingFace checkpoint +to a BF16 reference GGUF that transcribe.cpp's loader can ingest. Block +quantization (Q8_0, Q5_K_M, ...) is handled separately by +`tools/transcribe-quantize`. + +Source format: + HuggingFace repo/dir (OpenMOSS-Team/MOSS-Transcribe-Diarize), with: + + config.json MossTranscribeDiarizeConfig (audio_config + + text_config + fusion params) + generation_config.json Generation defaults + preprocessor_config.json WhisperFeatureExtractor parameters + processor_config.json audio_tokens_per_second / merge_size / + time_marker params (time-marker injection) + tokenizer.json Qwen2 BPE + 29 added special tokens (audio + placeholders at 151669/151670/151671) + vocab.json / merges.txt BPE base vocab + merges + chat_template.jinja Jinja chat template + model-00000-of-00001.safetensors BF16 weights (683 tensors) + +Architecture: audio-llm. HF state dict has three top-level prefixes: + + model.whisper_encoder.* -> HF WhisperEncoder (24 layers, gelu, + LayerNorm; conv1/conv2 + learned pos + embeddings). Emitted with the SAME tensor + names as the `whisper` family so the C++ + encoder graph can be shared. + model.vq_adaptor.* -> VQAdaptor bridge: Linear(4096->1024) -> + SiLU -> Linear(1024->1024) -> LayerNorm. + Consumes the 4x-time-merged encoder output. + model.language_model.* -> Qwen3-0.6B causal LM (28 layers, GQA 16/8, + head_dim=128, q/k RMSNorm, SwiGLU, standard + RoPE theta 1e6). Emitted with the SAME + names as the `qwen3_asr` text LM. + lm_head.weight -> NOT stored (tie_word_embeddings=true; the + loader reuses dec.token_embd.weight). + +Layout conversions: NONE. Linear weights are PyTorch (out, in); Conv1d +kernels are (out, in, k) — both already match ggml. + +KV emitted: + + general.architecture = "moss" + general.basename = "moss-transcribe-diarize" + general.languages = ["en", "zh"] + + stt.variant = "moss-transcribe-diarize" + stt.capability.speaker_diarization = true + + tokenizer.ggml.* (llama.cpp "gpt2" byte-level BPE, table padded to + the 151936-wide embedding/logits vocab) + stt.moss.encoder.* Whisper encoder hparams + stt.moss.decoder.* Qwen3 text-LM hparams + stt.moss.adaptor.* VQAdaptor dims + merge size + stt.moss.audio_token_id / audio_tokens_per_second / + time_marker_every_seconds / enable_time_marker (audio-span + + time-marker construction — see modeling/processing notes) + stt.frontend.* Whisper frontend parameters + +CLI: + + uv run --project scripts/envs/moss \ + scripts/convert-moss.py OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --revision d7231bbae2587a4af278735eb765b318c4f64edd + +Single-file, top-to-bottom. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from contextlib import ExitStack +from pathlib import Path + +import numpy as np +import torch +from gguf import GGMLQuantizationType, LlamaFileType +from safetensors import safe_open + + +class _ShardedSafetensors: + """Multi-file safe_open shim: reads model.safetensors.index.json (or a + single model.safetensors) and exposes keys()/get_tensor().""" + + def __init__(self, model_dir: Path) -> None: + self._stack = ExitStack() + single = model_dir / "model.safetensors" + if single.is_file(): + sf = self._stack.enter_context(safe_open(str(single), framework="pt")) + self._handles = {"__single__": sf} + self._shard_for = {k: "__single__" for k in sf.keys()} + return + index_path = model_dir / "model.safetensors.index.json" + with index_path.open() as f: + weight_map = json.load(f)["weight_map"] + shards = sorted(set(weight_map.values())) + self._handles = { + name: self._stack.enter_context( + safe_open(str(model_dir / name), framework="pt")) + for name in shards + } + self._shard_for = weight_map + + def __enter__(self): + return self + + def __exit__(self, *args): + self._stack.close() + + def keys(self): + return list(self._shard_for.keys()) + + def get_tensor(self, name: str): + return self._handles[self._shard_for[name]].get_tensor(name) + + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from lib.hf_source import download_snapshot, looks_like_repo_id # noqa: E402 +from lib.gguf_common import ( # noqa: E402 + gguf_writer, + TOKEN_TYPE_CONTROL, + TOKEN_TYPE_NORMAL, + add_general_identity, + encode_for_gguf, + gguf_name, + reference_dtype_for, + slug_from_repo_id, +) + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Canonical instruction from the OpenMOSS inference harness. The model has no +# language/task token, and the runtime supports only this fixed prompt. Prefix +# and suffix token IDs are baked into the GGUF so inference needs no BPE/Jinja +# renderer. +DEFAULT_PROMPT = ( + "请将音频转写为文本,每一段需以起始时间戳和说话人编号" + "([S01]、[S02]、[S03]…)开头,正文为对应的语音内容," + "并在段末标注结束时间戳,以清晰标明该段语音范围。" +) + +REFERENCE_DTYPE_LABEL = "BF16" +REFERENCE_FILE_TYPE = LlamaFileType.MOSTLY_BF16 +REFERENCE_GGML_TYPE = GGMLQuantizationType.BF16 + + +# --------------------------------------------------------------------------- +# Tokenizer extraction (Qwen2 byte-level BPE) +# --------------------------------------------------------------------------- +# +# Base vocab (vocab.json, 151643 tokens) + merges (merges.txt, 151388) + +# the 29 added special tokens from tokenizer.json (ids 151643..151671, +# including audio placeholders). The embedding/logits table is padded to +# text_config.vocab_size (151936); unused slots become <|unused_N|> +# placeholders so the token table length matches the model's logit width. + + +def extract_tokenizer(model_dir: Path, vocab_size: int) -> dict: + with (model_dir / "vocab.json").open(encoding="utf-8") as f: + base_vocab: dict[str, int] = json.load(f) + with (model_dir / "merges.txt").open(encoding="utf-8") as f: + merges = [ln.rstrip("\n") for ln in f] + if merges and merges[0].startswith("#"): + merges = merges[1:] + with (model_dir / "tokenizer.json").open(encoding="utf-8") as f: + tok_json = json.load(f) + with (model_dir / "tokenizer_config.json").open(encoding="utf-8") as f: + tokcfg = json.load(f) + + # id -> (token_str, is_special) + tok_by_id: dict[int, tuple[str, bool]] = {} + for tok, tid in base_vocab.items(): + tok_by_id[int(tid)] = (tok, False) + for entry in tok_json.get("added_tokens", []): + tok_by_id[int(entry["id"])] = (entry["content"], bool(entry.get("special", False))) + + content_to_id = {v[0]: k for k, v in tok_by_id.items()} + + max_id = max(tok_by_id.keys()) + if max_id + 1 > vocab_size: + raise ValueError(f"tokenizer id {max_id} exceeds vocab_size={vocab_size}") + + tokens: list[str] = [] + types: list[int] = [] + for i in range(vocab_size): + if i not in tok_by_id: + tokens.append(f"<|unused_{i}|>") + types.append(TOKEN_TYPE_NORMAL) + continue + tok, is_special = tok_by_id[i] + tokens.append(tok) + types.append(TOKEN_TYPE_CONTROL if is_special else TOKEN_TYPE_NORMAL) + + def _name_to_id(field: str) -> int | None: + tok = tokcfg.get(field) + if isinstance(tok, dict): + tok = tok.get("content") + if not tok: + return None + return content_to_id.get(tok) + + return { + "tokens": tokens, + "types": types, + "merges": merges, + "eos_id": _name_to_id("eos_token"), # <|im_end|> = 151645 + "pad_id": _name_to_id("pad_token"), # <|endoftext|> = 151643 + "bos_id": None, # Qwen: no BOS prepended + "audio_pad_id": content_to_id.get("<|audio_pad|>"), + "audio_start_id": content_to_id.get("<|audio_start|>"), + "audio_end_id": content_to_id.get("<|audio_end|>"), + } + + +# --------------------------------------------------------------------------- +# Hparams +# --------------------------------------------------------------------------- + + +def read_hparams(config: dict, preproc: dict, proc: dict) -> dict: + aenc = config["audio_config"] + tdec = config["text_config"] + + sample_rate = int(preproc.get("sampling_rate", 16000)) + hop_length = int(preproc["hop_length"]) + n_fft = int(preproc["n_fft"]) + n_mels = int(preproc["feature_size"]) + chunk_len = int(preproc.get("chunk_length", 30)) + n_samples = int(preproc.get("n_samples", chunk_len * sample_rate)) + nb_max_frm = int(preproc.get("nb_max_frames", n_samples // hop_length)) + + return { + # Audio encoder (HF WhisperEncoder) + "enc_n_layers": int(aenc["encoder_layers"]), + "enc_d_model": int(aenc["d_model"]), + "enc_n_heads": int(aenc["encoder_attention_heads"]), + "enc_ffn_dim": int(aenc["encoder_ffn_dim"]), + "enc_n_mels": int(aenc["num_mel_bins"]), + "enc_max_src_pos": int(aenc["max_source_positions"]), + "enc_activation": str(aenc["activation_function"]).lower(), + "enc_scale_embedding": bool(aenc.get("scale_embedding", False)), + + # Text LM (Qwen3-0.6B) + "dec_n_layers": int(tdec["num_hidden_layers"]), + "dec_hidden": int(tdec["hidden_size"]), + "dec_intermediate": int(tdec["intermediate_size"]), + "dec_n_heads": int(tdec["num_attention_heads"]), + "dec_n_kv_heads": int(tdec["num_key_value_heads"]), + "dec_head_dim": int(tdec["head_dim"]), + "dec_hidden_act": str(tdec["hidden_act"]).lower(), + "dec_rms_norm_eps": float(tdec["rms_norm_eps"]), + "dec_rope_theta": float(tdec["rope_theta"]), + "dec_max_pos_emb": int(tdec["max_position_embeddings"]), + "dec_tie_embeddings": bool(config.get("tie_word_embeddings", True)), + "dec_vocab_size": int(tdec["vocab_size"]), + + # Adaptor + fusion + "adaptor_input_dim": int(config["adaptor_input_dim"]), + "audio_merge_size": int(config["audio_merge_size"]), + "audio_token_id": int(config["audio_token_id"]), + + # Processor time-marker injection (audio-span construction) + "audio_tokens_per_second": float(proc["audio_tokens_per_second"]), + "time_marker_every_seconds": int(proc["time_marker_every_seconds"]), + "enable_time_marker": bool(proc["enable_time_marker"]), + + # Frontend (Whisper feature extractor) + "fe_sample_rate": sample_rate, + "fe_num_mels": n_mels, + "fe_n_fft": n_fft, + "fe_win_length": n_fft, + "fe_hop_length": hop_length, + "fe_window": "hann_periodic", + "fe_normalize": "per_utterance", + "fe_dither": float(preproc.get("dither", 0.0)), + "fe_pre_emphasis": 0.0, + "fe_f_min": 0.0, + "fe_f_max": float(sample_rate) / 2.0, + "fe_pad_mode": "reflect", + "fe_center": True, + "fe_mel_norm": "slaney", + "fe_chunk_length": chunk_len, + "fe_n_samples": n_samples, + "fe_nb_max_frm": nb_max_frm, + + "languages": ["en", "zh"], + } + + +# --------------------------------------------------------------------------- +# Tensor name maps (source HF name -> GGUF name) +# --------------------------------------------------------------------------- + +# Whisper encoder top-level (matches the `whisper` family names exactly). +ENC_TOP_TABLE: list[tuple[str, str]] = [ + ("model.whisper_encoder.conv1.weight", "enc.conv.0.weight"), + ("model.whisper_encoder.conv1.bias", "enc.conv.0.bias"), + ("model.whisper_encoder.conv2.weight", "enc.conv.1.weight"), + ("model.whisper_encoder.conv2.bias", "enc.conv.1.bias"), + ("model.whisper_encoder.embed_positions.weight", "enc.pos_emb.weight"), + ("model.whisper_encoder.layer_norm.weight", "enc.final_norm.weight"), + ("model.whisper_encoder.layer_norm.bias", "enc.final_norm.bias"), +] + +# Whisper attention: q / v / out have bias, k does NOT. +ENC_BLOCK_TABLE: list[tuple[str, str]] = [ + ("self_attn_layer_norm.weight", "norm_attn.weight"), + ("self_attn_layer_norm.bias", "norm_attn.bias"), + ("self_attn.q_proj.weight", "attn.q.weight"), + ("self_attn.q_proj.bias", "attn.q.bias"), + ("self_attn.k_proj.weight", "attn.k.weight"), + ("self_attn.v_proj.weight", "attn.v.weight"), + ("self_attn.v_proj.bias", "attn.v.bias"), + ("self_attn.out_proj.weight", "attn.out.weight"), + ("self_attn.out_proj.bias", "attn.out.bias"), + ("final_layer_norm.weight", "norm_ffn.weight"), + ("final_layer_norm.bias", "norm_ffn.bias"), + ("fc1.weight", "ffn.fc1.weight"), + ("fc1.bias", "ffn.fc1.bias"), + ("fc2.weight", "ffn.fc2.weight"), + ("fc2.bias", "ffn.fc2.bias"), +] + +# VQAdaptor: Sequential(Linear[0], SiLU[1], Linear[2], LayerNorm[3]). +# norm_out.* hits the F32 bucket via the "norm_" rule in reference_dtype_for. +ADAPTOR_TABLE: list[tuple[str, str]] = [ + ("model.vq_adaptor.layers.0.weight", "adaptor.fc1.weight"), + ("model.vq_adaptor.layers.0.bias", "adaptor.fc1.bias"), + ("model.vq_adaptor.layers.2.weight", "adaptor.fc2.weight"), + ("model.vq_adaptor.layers.2.bias", "adaptor.fc2.bias"), + ("model.vq_adaptor.layers.3.weight", "adaptor.norm_out.weight"), + ("model.vq_adaptor.layers.3.bias", "adaptor.norm_out.bias"), +] + +# Qwen3 text LM top-level (tied head: lm_head not stored). +DEC_TOP_TABLE: list[tuple[str, str]] = [ + ("model.language_model.embed_tokens.weight", "dec.token_embd.weight"), + ("model.language_model.norm.weight", "dec.output_norm.weight"), +] + +# Qwen3 per-layer (q/k RMSNorm, no attention biases). +DEC_BLOCK_TABLE: list[tuple[str, str]] = [ + ("input_layernorm.weight", "norm_attn.weight"), + ("post_attention_layernorm.weight", "norm_ffn.weight"), + ("self_attn.q_proj.weight", "attn.q.weight"), + ("self_attn.k_proj.weight", "attn.k.weight"), + ("self_attn.v_proj.weight", "attn.v.weight"), + ("self_attn.o_proj.weight", "attn.o.weight"), + ("self_attn.q_norm.weight", "attn.q_norm.weight"), + ("self_attn.k_norm.weight", "attn.k_norm.weight"), + ("mlp.gate_proj.weight", "ffn.gate.weight"), + ("mlp.up_proj.weight", "ffn.up.weight"), + ("mlp.down_proj.weight", "ffn.down.weight"), +] + +# tie_word_embeddings=true — lm_head.weight is not present in the state dict. +SKIP_EXACT: set[str] = set() + + +def compute_prompt_tokens(model_dir: Path) -> dict: + """Bake the fixed prompt token ids via the reference processor. + + Mirrors processing_moss_transcribe_diarize._expand_audio_token: render the + chat prompt (system + user + audio placeholder + DEFAULT_PROMPT + + add_generation_prompt), split on the single <|audio_pad|> token, and encode + the before/after halves with add_special_tokens=False. The C++ prompt + builder inserts the deterministic audio span (audio_span_ids) between them, + so the reconstructed input_ids are identical to the reference processor's. + Also bakes the single-token ids for digits 0..9 (marker construction). + """ + from transformers import AutoProcessor + + proc = AutoProcessor.from_pretrained( + str(model_dir), trust_remote_code=True, local_files_only=True) + tokenizer = proc.tokenizer + audio_tok = getattr(proc, "audio_token", "<|audio_pad|>") + + messages = [{ + "role": "user", + "content": [ + {"type": "audio", "audio": "audio.wav"}, + {"type": "text", "text": DEFAULT_PROMPT}, + ], + }] + prompt_text = proc.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True) + if prompt_text.count(audio_tok) != 1: + raise ValueError( + f"expected exactly one {audio_tok!r} in rendered prompt, " + f"got {prompt_text.count(audio_tok)}") + before_audio, after_audio = prompt_text.split(audio_tok, maxsplit=1) + prefix_ids = [int(i) for i in tokenizer.encode(before_audio, add_special_tokens=False)] + suffix_ids = [int(i) for i in tokenizer.encode(after_audio, add_special_tokens=False)] + + digit_ids = [] + for d in "0123456789": + ids = tokenizer.encode(d, add_special_tokens=False) + if len(ids) != 1: + raise ValueError(f"digit {d!r} is not a single token: {ids}") + digit_ids.append(int(ids[0])) + + print(f"Prompt tokens: prefix={len(prefix_ids)} suffix={len(suffix_ids)} " + f"digits={digit_ids}") + return {"prefix_ids": prefix_ids, "suffix_ids": suffix_ids, "digit_ids": digit_ids} + + +def compute_size_label(total_params: int) -> str: + if total_params >= 1_000_000_000: + return f"{total_params / 1_000_000_000:.1f}B" + if total_params >= 1_000_000: + return f"{total_params / 1_000_000:.0f}M" + return f"{total_params / 1_000:.0f}K" + + +# --------------------------------------------------------------------------- +# Main converter +# --------------------------------------------------------------------------- + + +def convert(model_dir: Path, out_path: Path, variant: str, repo_id: str | None = None) -> None: + print(f"Output dtype: {REFERENCE_DTYPE_LABEL} (source/reference dtype)") + + config_path = model_dir / "config.json" + preproc_path = model_dir / "preprocessor_config.json" + proc_path = model_dir / "processor_config.json" + chat_template_path = model_dir / "chat_template.jinja" + + for p in (config_path, preproc_path, proc_path): + if not p.is_file(): + raise FileNotFoundError(f"missing required file: {p}") + + with config_path.open() as f: + config = json.load(f) + with preproc_path.open() as f: + preproc = json.load(f) + with proc_path.open() as f: + proc = json.load(f) + chat_template = None + if chat_template_path.is_file(): + chat_template = chat_template_path.read_text(encoding="utf-8") + + hp = read_hparams(config, preproc, proc) + print(f"Audio encoder: {hp['enc_n_layers']} layers, d_model={hp['enc_d_model']}, " + f"heads={hp['enc_n_heads']}, act={hp['enc_activation']}") + print(f"Adaptor: {hp['adaptor_input_dim']} -> {hp['dec_hidden']} " + f"(merge={hp['audio_merge_size']})") + print(f"Text LM: {hp['dec_n_layers']} layers, hidden={hp['dec_hidden']}, " + f"heads={hp['dec_n_heads']}/{hp['dec_n_kv_heads']}, " + f"tie_embeddings={hp['dec_tie_embeddings']}") + print(f"Time-marker: every {hp['time_marker_every_seconds']}s, " + f"{hp['audio_tokens_per_second']} tok/s, enabled={hp['enable_time_marker']}") + + print(f"Reading tokenizer from {model_dir}") + tok = extract_tokenizer(model_dir, hp["dec_vocab_size"]) + if tok["audio_pad_id"] != hp["audio_token_id"]: + raise ValueError( + f"audio_pad token id mismatch: config={hp['audio_token_id']} " + f"tokenizer={tok['audio_pad_id']}") + + print("Baking fixed prompt token ids from the reference processor") + prompt = compute_prompt_tokens(model_dir) + + print(f"Opening safetensors under {model_dir}") + with _ShardedSafetensors(model_dir) as st: + st_keys = set(st.keys()) + + total = sum(st.get_tensor(k).numel() for k in st_keys if k not in SKIP_EXACT) + size_label = compute_size_label(total) + print(f"Total params: {total:,} -> size_label={size_label}") + + print(f"Writing GGUF to {out_path}") + writer = gguf_writer(str(out_path), "moss") + + # ---- general.* ---- + add_general_identity( + writer, + name="MOSS-Transcribe-Diarize 0.9B", + basename="moss-transcribe-diarize", + size_label=size_label, + file_type=REFERENCE_FILE_TYPE, + languages=hp["languages"], + author="MOSI.AI", + organization="OpenMOSS-Team", + license="apache-2.0", + license_name="Apache License 2.0", + license_link="https://www.apache.org/licenses/LICENSE-2.0", + repo_url=(f"https://huggingface.co/{repo_id}" if repo_id else None), + ) + + writer.add_string("stt.variant", variant) + + # Speaker labels are generated inline as [Sxx] text. + writer.add_bool("stt.capability.speaker_diarization", True) + + # ---- tokenizer.ggml.* (llama.cpp "gpt2" byte-level BPE) ---- + writer.add_string("tokenizer.ggml.model", "gpt2") + writer.add_string("tokenizer.ggml.pre", "qwen2") + writer.add_array("tokenizer.ggml.tokens", tok["tokens"]) + writer.add_array("tokenizer.ggml.token_type", tok["types"]) + writer.add_array("tokenizer.ggml.merges", tok["merges"]) + if tok["eos_id"] is not None: + writer.add_uint32("tokenizer.ggml.eos_token_id", tok["eos_id"]) + if tok["pad_id"] is not None: + writer.add_uint32("tokenizer.ggml.padding_token_id", tok["pad_id"]) + writer.add_bool("tokenizer.ggml.add_bos_token", False) + if chat_template is not None: + writer.add_string("tokenizer.chat_template", chat_template) + + # ---- stt.moss.encoder.* (Whisper) ---- + writer.add_uint32("stt.moss.encoder.n_layers", hp["enc_n_layers"]) + writer.add_uint32("stt.moss.encoder.d_model", hp["enc_d_model"]) + writer.add_uint32("stt.moss.encoder.n_heads", hp["enc_n_heads"]) + writer.add_uint32("stt.moss.encoder.ffn_dim", hp["enc_ffn_dim"]) + writer.add_uint32("stt.moss.encoder.num_mel_bins", hp["enc_n_mels"]) + writer.add_uint32("stt.moss.encoder.max_source_positions", hp["enc_max_src_pos"]) + writer.add_string("stt.moss.encoder.activation", hp["enc_activation"]) + writer.add_bool("stt.moss.encoder.scale_embedding", hp["enc_scale_embedding"]) + + # ---- stt.moss.decoder.* (Qwen3 text LM) ---- + writer.add_uint32("stt.moss.decoder.n_layers", hp["dec_n_layers"]) + writer.add_uint32("stt.moss.decoder.hidden_size", hp["dec_hidden"]) + writer.add_uint32("stt.moss.decoder.intermediate_size", hp["dec_intermediate"]) + writer.add_uint32("stt.moss.decoder.n_heads", hp["dec_n_heads"]) + writer.add_uint32("stt.moss.decoder.n_kv_heads", hp["dec_n_kv_heads"]) + writer.add_uint32("stt.moss.decoder.head_dim", hp["dec_head_dim"]) + writer.add_string("stt.moss.decoder.hidden_act", hp["dec_hidden_act"]) + writer.add_float32("stt.moss.decoder.rms_norm_eps", hp["dec_rms_norm_eps"]) + writer.add_float32("stt.moss.decoder.rope_theta", hp["dec_rope_theta"]) + writer.add_uint32("stt.moss.decoder.max_position_embeddings", hp["dec_max_pos_emb"]) + writer.add_bool("stt.moss.decoder.tie_word_embeddings", hp["dec_tie_embeddings"]) + writer.add_uint32("stt.moss.decoder.vocab_size", hp["dec_vocab_size"]) + + # ---- stt.moss.adaptor.* + fusion / time-marker ---- + writer.add_uint32("stt.moss.adaptor.input_dim", hp["adaptor_input_dim"]) + writer.add_uint32("stt.moss.audio_merge_size", hp["audio_merge_size"]) + writer.add_uint32("stt.moss.audio_token_id", hp["audio_token_id"]) + writer.add_float32("stt.moss.audio_tokens_per_second", hp["audio_tokens_per_second"]) + writer.add_uint32("stt.moss.time_marker_every_seconds", hp["time_marker_every_seconds"]) + writer.add_bool("stt.moss.enable_time_marker", hp["enable_time_marker"]) + + # ---- baked fixed prompt (see compute_prompt_tokens) ---- + # C++ builds input_ids = prompt_prefix_tokens + audio_span + prompt_suffix_tokens. + writer.add_array("stt.moss.prompt_prefix_tokens", prompt["prefix_ids"]) + writer.add_array("stt.moss.prompt_suffix_tokens", prompt["suffix_ids"]) + writer.add_array("stt.moss.digit_tokens", prompt["digit_ids"]) + + # ---- stt.frontend.* (Whisper feature extractor) ---- + writer.add_string("stt.frontend.type", "mel") + writer.add_uint32("stt.frontend.num_mels", hp["fe_num_mels"]) + writer.add_uint32("stt.frontend.sample_rate", hp["fe_sample_rate"]) + writer.add_uint32("stt.frontend.n_fft", hp["fe_n_fft"]) + writer.add_uint32("stt.frontend.win_length", hp["fe_win_length"]) + writer.add_uint32("stt.frontend.hop_length", hp["fe_hop_length"]) + writer.add_string("stt.frontend.window", hp["fe_window"]) + writer.add_string("stt.frontend.normalize", hp["fe_normalize"]) + writer.add_float32("stt.frontend.dither", hp["fe_dither"]) + writer.add_float32("stt.frontend.pre_emphasis", hp["fe_pre_emphasis"]) + writer.add_float32("stt.frontend.f_min", hp["fe_f_min"]) + writer.add_float32("stt.frontend.f_max", hp["fe_f_max"]) + writer.add_string("stt.frontend.pad_mode", hp["fe_pad_mode"]) + writer.add_bool("stt.frontend.center", hp["fe_center"]) + writer.add_string("stt.frontend.mel_norm", hp["fe_mel_norm"]) + writer.add_uint32("stt.frontend.chunk_length", hp["fe_chunk_length"]) + writer.add_uint32("stt.frontend.n_samples", hp["fe_n_samples"]) + writer.add_uint32("stt.frontend.nb_max_frames", hp["fe_nb_max_frm"]) + + # ---- tensors ---- + n_added = bytes_in = bytes_out = 0 + + # Frontend buffers: slaney mel filterbank + Whisper periodic Hann + # window, baked in so the C++ MelFrontend is bit-identical. + import librosa as _lb + mel_fb = _lb.filters.mel( + sr=hp["fe_sample_rate"], n_fft=hp["fe_n_fft"], n_mels=hp["fe_num_mels"], + fmin=hp["fe_f_min"], fmax=hp["fe_f_max"], norm="slaney", htk=False, + ).astype(np.float32) + writer.add_tensor("frontend.mel_filterbank", np.ascontiguousarray(mel_fb), + raw_dtype=GGMLQuantizationType.F32) + N = int(hp["fe_win_length"]) + hann = (0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(N) / N)).astype(np.float32) + writer.add_tensor("frontend.window", np.ascontiguousarray(hann), + raw_dtype=GGMLQuantizationType.F32) + n_added += 2 + bytes_in += mel_fb.nbytes + hann.nbytes + bytes_out += mel_fb.nbytes + hann.nbytes + + def add(src_name: str, dst_name: str) -> None: + nonlocal n_added, bytes_in, bytes_out + if src_name not in st_keys: + raise KeyError(f"safetensors missing tensor: {src_name!r}") + t = st.get_tensor(src_name) + if t.dtype != torch.bfloat16: + raise ValueError(f"{src_name}: expected torch.bfloat16, got {t.dtype}") + arr = np.ascontiguousarray(t.float().numpy()) + target_type = reference_dtype_for(dst_name, REFERENCE_GGML_TYPE) + encoded, raw_dtype = encode_for_gguf(arr, target_type) + writer.add_tensor(dst_name, encoded, raw_dtype=raw_dtype) + bytes_in += int(arr.nbytes) + bytes_out += int(encoded.nbytes) + n_added += 1 + + for src, dst in ENC_TOP_TABLE: + add(src, dst) + for i in range(hp["enc_n_layers"]): + for s, d in ENC_BLOCK_TABLE: + add(f"model.whisper_encoder.layers.{i}.{s}", f"enc.blocks.{i}.{d}") + for src, dst in ADAPTOR_TABLE: + add(src, dst) + for src, dst in DEC_TOP_TABLE: + add(src, dst) + for i in range(hp["dec_n_layers"]): + for s, d in DEC_BLOCK_TABLE: + add(f"model.language_model.layers.{i}.{s}", f"dec.blocks.{i}.{d}") + + expected = ( + len(ENC_TOP_TABLE) + + hp["enc_n_layers"] * len(ENC_BLOCK_TABLE) + + len(ADAPTOR_TABLE) + + len(DEC_TOP_TABLE) + + hp["dec_n_layers"] * len(DEC_BLOCK_TABLE) + + 2 # frontend buffers + ) + if n_added != expected: + raise RuntimeError(f"tensor count mismatch: added {n_added}, expected {expected}") + print(f"Added {n_added} tensors " + f"({bytes_in / (1024 * 1024):.1f} MB fp32 -> " + f"{bytes_out / (1024 * 1024):.1f} MB on disk)") + + # Warn about unconsumed safetensors keys. + consumed = set(SKIP_EXACT) + for src, _ in ENC_TOP_TABLE: + consumed.add(src) + for i in range(hp["enc_n_layers"]): + for s, _ in ENC_BLOCK_TABLE: + consumed.add(f"model.whisper_encoder.layers.{i}.{s}") + for src, _ in ADAPTOR_TABLE: + consumed.add(src) + for src, _ in DEC_TOP_TABLE: + consumed.add(src) + for i in range(hp["dec_n_layers"]): + for s, _ in DEC_BLOCK_TABLE: + consumed.add(f"model.language_model.layers.{i}.{s}") + unused = sorted(st_keys - consumed) + if unused: + print(f"WARNING: {len(unused)} safetensors keys not consumed:", file=sys.stderr) + for k in unused[:20]: + print(f" {k}", file=sys.stderr) + + print("Writing header + KV + tensor info...") + writer.write_header_to_file() + writer.write_kv_data_to_file() + print("Writing tensor data...") + writer.write_tensors_to_file() + writer.close() + + print(f"Done. Wrote {out_path} ({out_path.stat().st_size / (1024 * 1024):.1f} MB)") + + +def main(argv: list[str]) -> int: + p = argparse.ArgumentParser( + description="Convert a MOSS-Transcribe-Diarize checkpoint to a BF16 GGUF.") + p.add_argument("model", type=str, + help="HF repo id (OpenMOSS-Team/MOSS-Transcribe-Diarize) or local dir") + p.add_argument("out_path", type=Path, nargs="?", + help="Output .gguf path (derived from --repo-id when omitted)") + p.add_argument("--repo-id", type=str, default=None, + help="HF repo id used to derive the output slug from a local path") + p.add_argument("--revision", type=str, default=None, + help="HF revision (branch/tag/commit SHA) to pin the download to. " + "Ignored when `model` is a local directory.") + p.add_argument("--variant", type=str, default=None, + help="stt.variant string (default: derived from slug)") + args = p.parse_args(argv[1:]) + + if looks_like_repo_id(args.model): + repo_id = args.repo_id or args.model + model_dir = download_snapshot(args.model, args.revision) + else: + model_dir = Path(args.model) + if not model_dir.is_dir(): + print(f"error: {model_dir} is not a directory and not an HF repo id", file=sys.stderr) + return 2 + repo_id = args.repo_id + + out_path = args.out_path + if out_path is None: + if not repo_id: + print("error: provide out_path, --repo-id, or pass an HF repo id as model", + file=sys.stderr) + return 2 + slug = slug_from_repo_id(repo_id) + out_path = REPO_ROOT / "models" / slug / gguf_name(slug, REFERENCE_DTYPE_LABEL) + out_path.parent.mkdir(parents=True, exist_ok=True) + + variant = args.variant or (slug_from_repo_id(repo_id).lower() if repo_id else out_path.stem) + convert(model_dir, out_path, variant, repo_id=repo_id) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/scripts/dump_reference_moss_author.py b/scripts/dump_reference_moss_author.py new file mode 100644 index 00000000..72ddb131 --- /dev/null +++ b/scripts/dump_reference_moss_author.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +""" +dump_reference_moss_author.py - generate MOSS-Transcribe-Diarize reference +tensors from the OpenMOSS custom trust_remote_code implementation (loaded +via transformers AutoModelForCausalLM). + +Architecture (see modeling_moss_transcribe_diarize.py): + log-mel input_features -> WhisperEncoder (24 layers) + -> 4x time merge (B, T, 1024) -> (B, T/4, 4096) + -> VQAdaptor (Linear 4096->1024, SiLU, Linear 1024->1024, LayerNorm) + -> masked_scatter into Qwen3-0.6B text embeddings at <|audio_pad|> (151671) + -> Qwen3 decoder (28 layers) -> lm_head -> logits + +Usage: + + uv run --project scripts/envs/moss \ + scripts/dump_reference_moss_author.py decode \ + --model OpenMOSS-Team/MOSS-Transcribe-Diarize \ + --audio samples/jfk.wav \ + --out build/validate/moss/moss-transcribe-diarize/jfk/decode/ref + +Writes: + .f32 raw little-endian fp32, row-major (leading batch squeezed) + .json per-tensor sidecar (rms + p99_abs for tolerances) + transcript.json behavioral artifact (de-diarized text + raw + token ids) + +Dump points (match tests/tolerances/moss.json): + + enc.mel.in input log-mel after WhisperFeatureExtractor + enc.pos_add.out whisper hidden after conv stack + positional add + enc.block..out selected whisper encoder layer outputs + enc.ln_post.out after whisper encoder final layer_norm + enc.merge.out after 4x temporal merge (adaptor input, dim 4096) + enc.adaptor.out after VQAdaptor (injected audio features, dim 1024) + dec.token_emb LM input embeddings pre audio injection + dec.audio_injected LM embeddings after masked_scatter of audio features + dec.block..out selected Qwen3 decoder layer outputs + dec.out_before_head after final Qwen3 RMSNorm + dec.logits_raw first-step pre-softmax next-token logits +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Any + +import numpy as np + + +# The canonical timestamped+diarize instruction shipped in the OpenMOSS +# GitHub inference harness (moss_transcribe_diarize/inference_utils.py +# DEFAULT_PROMPT). Used for English audio as well; the model has no +# language/task token. +DEFAULT_PROMPT = ( + "请将音频转写为文本,每一段需以起始时间戳和说话人编号" + "([S01]、[S02]、[S03]…)开头,正文为对应的语音内容," + "并在段末标注结束时间戳,以清晰标明该段语音范围。" +) + + +# --------------------------------------------------------------------------- +# Shared reference-dump helpers +# --------------------------------------------------------------------------- + + +def configure_torch(args: argparse.Namespace) -> None: + import torch + + torch.manual_seed(0) + if args.torch_threads > 0: + torch.set_num_threads(args.torch_threads) + torch.set_num_interop_threads(1) + try: + torch.use_deterministic_algorithms(True, warn_only=True) + except TypeError: + torch.use_deterministic_algorithms(True) + + +def to_np(t) -> np.ndarray: + import torch + + if isinstance(t, torch.Tensor): + a = t.detach().to(dtype=torch.float32, device="cpu").numpy() + else: + a = np.asarray(t, dtype=np.float32) + while a.ndim > 1 and a.shape[0] == 1: + a = a[0] + return np.ascontiguousarray(a, dtype=np.float32) + + +def write_dump( + out_dir: Path, + name: str, + data: np.ndarray, + *, + source: dict[str, Any], + stage: str, +) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + if data.dtype != np.float32: + raise ValueError(f"only float32 tensors supported, got {data.dtype}") + if not data.flags.c_contiguous: + data = np.ascontiguousarray(data) + (out_dir / f"{name}.f32").write_bytes(data.tobytes()) + if data.size: + flat64 = data.astype(np.float64, copy=False).reshape(-1) + rms = float(np.sqrt(np.mean(flat64 * flat64))) + p99_abs = float(np.quantile(np.abs(flat64), 0.99)) + a_min, a_max, a_mean = float(data.min()), float(data.max()), float(flat64.mean()) + else: + rms = p99_abs = a_min = a_max = a_mean = 0.0 + meta = { + "name": name, + "stage": stage, + "shape": list(data.shape), + "dtype": "f32", + "layout": "row-major", + "min": a_min, + "max": a_max, + "mean": a_mean, + "rms": rms, + "p99_abs": p99_abs, + "source": source, + } + (out_dir / f"{name}.json").write_text(json.dumps(meta, indent=2) + "\n") + print(f" wrote {out_dir / f'{name}.f32'} " + f"({data.size * 4} bytes, shape={list(data.shape)})") + + +def load_audio(audio_path: Path) -> tuple[np.ndarray, int]: + import soundfile as sf + + pcm, sr = sf.read(str(audio_path), dtype="float32", always_2d=False) + if pcm.ndim > 1: + pcm = pcm.mean(axis=1) + return np.ascontiguousarray(pcm, dtype=np.float32), int(sr) + + +def dediarize(raw: str) -> str: + """Strip [start], [Sxx], [end] bracket spans from the canonical + [start][Sxx]text[end] format, leaving plain transcription text. All + metadata (timestamps + speaker labels) lives inside brackets; text + lives outside them.""" + stripped = re.sub(r"\[[^\]]*\]", " ", raw) + return " ".join(stripped.split()) + + +def normalize_text(text: str) -> str: + return " ".join(text.strip().lower().split()) + + +# --------------------------------------------------------------------------- +# Model / processor loading +# --------------------------------------------------------------------------- + + +def resolve_model(raw: str) -> tuple[str, bool]: + p = Path(raw).expanduser().resolve() + if p.is_dir(): + return str(p), True + return raw, False + + +def load_reference(args: argparse.Namespace): + import torch + import transformers + from transformers import AutoModelForCausalLM, AutoProcessor + + model_id, local_only = resolve_model(args.model) + source = "local path" if local_only else "HuggingFace" + revision = getattr(args, "revision", None) if not local_only else None + pin_note = f"@{revision}" if revision else "" + print( + f"Loading MOSS-Transcribe-Diarize from {model_id}{pin_note} ({source}, " + f"transformers {transformers.__version__}, device={args.device})" + ) + + dtype = {"bf16": torch.bfloat16, "f16": torch.float16, "f32": torch.float32}[args.dtype] + processor = AutoProcessor.from_pretrained( + model_id, revision=revision, + trust_remote_code=True, local_files_only=local_only, + ) + model = AutoModelForCausalLM.from_pretrained( + model_id, + revision=revision, + trust_remote_code=True, + local_files_only=local_only, + dtype=dtype, + attn_implementation="eager", # hookable + deterministic + ).eval() + model.to(args.device) + return processor, model + + +def model_dtype_name(model) -> str: + import torch + + dtype = next(model.parameters()).dtype + return { + torch.bfloat16: "bf16", + torch.float16: "f16", + torch.float32: "f32", + }.get(dtype, str(dtype).removeprefix("torch.")) + + +def make_source( + *, + args: argparse.Namespace, + model_id: str, + audio_path: Path, + n_samples: int, + sample_rate: int, + model_dtype: str, +) -> dict[str, Any]: + import torch + import transformers + + return { + "kind": "moss-author-repo", + "transformers_version": transformers.__version__, + "torch_version": torch.__version__, + "model": model_id, + "model_dtype": model_dtype, + "device": args.device, + "torch_threads": args.torch_threads, + "audio": audio_path.name, + "n_samples": int(n_samples), + "sample_rate": int(sample_rate), + "prompt": "DEFAULT_PROMPT (timestamp+diarize, zh)", + } + + +# --------------------------------------------------------------------------- +# Forward hooks +# --------------------------------------------------------------------------- + + +class CaptureHook: + """Record the first output tensor a module emits during forward.""" + + def __init__(self) -> None: + self.value = None + + def __call__(self, module, inputs, output) -> None: + if self.value is not None: + return + if isinstance(output, tuple): + output = output[0] + self.value = output + + +def register_encoder_hooks(enc, blocks: list[int]) -> dict[str, CaptureHook]: + """Hook the WhisperEncoder at standard dump points. + + WhisperEncoder.forward: conv1 -> gelu -> conv2 -> gelu -> permute + -> + embed_positions -> layers[i]... -> layer_norm + """ + hooks: dict[str, CaptureHook] = {} + + def hook_forward(name: str, module) -> None: + h = CaptureHook() + module.register_forward_hook(h) + hooks[name] = h + + for i in blocks: + if 0 <= i < len(enc.layers): + hook_forward(f"enc.block.{i}.out", enc.layers[i]) + hook_forward("enc.ln_post.out", enc.layer_norm) + + # "after conv stack + positional add" = input to encoder layer[0]. + pos_add = CaptureHook() + + def pre0(module, args): + if pos_add.value is None and len(args) >= 1: + pos_add.value = args[0] + + enc.layers[0].register_forward_pre_hook(pre0) + hooks["enc.pos_add.out"] = pos_add + return hooks + + +def register_adaptor_hooks(vq_adaptor) -> dict[str, CaptureHook]: + """Capture the 4x-merged adaptor input (dim 4096) and adaptor output + (dim 1024, the injected audio features).""" + hooks: dict[str, CaptureHook] = {} + + out_hook = CaptureHook() + vq_adaptor.register_forward_hook(out_hook) + hooks["enc.adaptor.out"] = out_hook + + merge_hook = CaptureHook() + + def pre(module, args): + if merge_hook.value is None and len(args) >= 1: + merge_hook.value = args[0] + + vq_adaptor.register_forward_pre_hook(pre) + hooks["enc.merge.out"] = merge_hook + return hooks + + +def register_decoder_hooks(text_model, blocks: list[int]) -> dict[str, CaptureHook]: + """Hook the Qwen3 text model (language_model) at standard dump points.""" + hooks: dict[str, CaptureHook] = {} + + def hook_forward(name: str, module) -> None: + h = CaptureHook() + module.register_forward_hook(h) + hooks[name] = h + + hook_forward("dec.token_emb", text_model.embed_tokens) + for i in blocks: + if 0 <= i < len(text_model.layers): + hook_forward(f"dec.block.{i}.out", text_model.layers[i]) + hook_forward("dec.out_before_head", text_model.norm) + return hooks + + +def register_injection_hook(backbone) -> dict[str, CaptureHook]: + """Capture the LM input embeddings after masked_scatter of audio + features (inputs_embeds kwarg passed to language_model.forward).""" + hooks: dict[str, CaptureHook] = {} + inj = CaptureHook() + + def pre(module, args, kwargs): + if inj.value is not None: + return + emb = kwargs.get("inputs_embeds") + if emb is not None: + inj.value = emb + + backbone.language_model.register_forward_pre_hook(pre, with_kwargs=True) + hooks["dec.audio_injected"] = inj + return hooks + + +# --------------------------------------------------------------------------- +# decode subcommand +# --------------------------------------------------------------------------- + + +def cmd_decode(args: argparse.Namespace) -> int: + import torch + + configure_torch(args) + processor, model = load_reference(args) + backbone = model.model # MossTranscribeDiarizeModel + enc = backbone.whisper_encoder + text_model = backbone.language_model + + pcm, sr = load_audio(Path(args.audio)) + if sr != 16000: + print(f"error: expected 16 kHz audio, got {sr}", file=sys.stderr) + return 2 + print(f"Loaded {Path(args.audio).name}: {pcm.size} samples @ {sr} Hz") + + enc_blocks = sorted({0, len(enc.layers) - 1, *args.enc_blocks}) + dec_blocks = sorted({0, text_model.config.num_hidden_layers - 1, *args.dec_blocks}) + + enc_hooks = register_encoder_hooks(enc, enc_blocks) + adaptor_hooks = register_adaptor_hooks(backbone.vq_adaptor) + dec_hooks = register_decoder_hooks(text_model, dec_blocks) + inj_hooks = register_injection_hook(backbone) + + # Mirror the reference harness (inference_utils.prepare_inputs): + # apply_chat_template(add_generation_prompt=True) then processor(text, audio). + messages = [ + { + "role": "user", + "content": [ + {"type": "audio", "audio": str(args.audio)}, + {"type": "text", "text": DEFAULT_PROMPT}, + ], + } + ] + prompt_text = processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + inputs = processor(text=prompt_text, audio=[pcm], return_tensors="pt") + inputs = inputs.to(args.device) + + input_features = inputs.get("input_features") + if input_features is None: + print("error: processor did not return input_features", file=sys.stderr) + return 3 + + source = make_source( + args=args, + model_id=str(Path(args.model).expanduser().resolve()) + if Path(args.model).is_dir() else args.model, + audio_path=Path(args.audio), + n_samples=pcm.size, + sample_rate=sr, + model_dtype=model_dtype_name(model), + ) + + out_dir = Path(args.out).expanduser().resolve() + + # enc.mel.in: log-mel [n_chunks, n_mels, T]; squeeze single chunk to [n_mels, T]. + write_dump(out_dir, "enc.mel.in", to_np(input_features), + source=source, stage="frontend.mel.norm") + + with torch.inference_mode(): + gen = model.generate( + input_ids=inputs["input_ids"], + attention_mask=inputs["attention_mask"], + input_features=inputs["input_features"], + audio_feature_lengths=inputs["audio_feature_lengths"], + audio_chunk_mapping=inputs["audio_chunk_mapping"], + max_new_tokens=args.max_new_tokens, + do_sample=False, + num_beams=1, + return_dict_in_generate=True, + output_scores=True, + ) + + prompt_len = int(inputs["attention_mask"][0].sum().item()) + seq = gen.sequences[0].detach().cpu().tolist() + gen_ids = seq[prompt_len:] + eos_id = processor.tokenizer.eos_token_id + if eos_id in gen_ids: + gen_ids = gen_ids[:gen_ids.index(eos_id)] + raw_text = processor.tokenizer.decode(gen_ids, skip_special_tokens=True).strip() + plain_text = dediarize(raw_text) + + def flush(name: str, value, stage: str) -> None: + if value is None: + print(f" WARN: no capture for {name}") + return + write_dump(out_dir, name, to_np(value), source=source, stage=stage) + + for name, hook in enc_hooks.items(): + flush(name, hook.value, stage=name) + for name, hook in adaptor_hooks.items(): + flush(name, hook.value, stage=name) + for name, hook in inj_hooks.items(): + flush(name, hook.value, stage=name) + for name, hook in dec_hooks.items(): + flush(name, hook.value, stage=name) + if gen.scores: + flush("dec.logits_raw", gen.scores[0], stage="dec.logits_raw") + # Mid-generation logits (n_past > 0) — KV-cache decode coverage. Step 8 = + # the logits that select the 9th generated token; the C++ runner dumps the + # matching step (greedy paths agree, so the tokens line up). + if len(gen.scores) > 8: + flush("dec.logits_raw.gen8", gen.scores[8], stage="dec.logits_raw.gen") + + transcript = { + "schema": "transcribe-reference-transcript-v1", + "text": plain_text, + "raw_text": raw_text, + "normalized_text": normalize_text(plain_text), + "token_ids": gen_ids, + "prompt_len": prompt_len, + "generation": { + "do_sample": False, + "num_beams": 1, + "max_new_tokens": args.max_new_tokens, + }, + "source": source, + } + (out_dir / "transcript.json").write_text( + json.dumps(transcript, indent=2, ensure_ascii=False) + "\n" + ) + print(f"Raw transcript: {raw_text!r}") + print(f"Plain transcript: {plain_text!r}") + print(f"Wrote transcript.json ({len(gen_ids)} tokens)") + return 0 + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def _add_common(sp: argparse.ArgumentParser) -> None: + sp.add_argument("--model", required=True, + help="HF repo id (OpenMOSS-Team/MOSS-Transcribe-Diarize) or local dir") + sp.add_argument("--revision", default=None, + help="HF revision to pin (ignored for local paths).") + sp.add_argument("--audio", required=True, help="16 kHz mono WAV path") + sp.add_argument("--out", required=True, help="Output directory for dumps") + sp.add_argument("--device", default="cpu", help="torch device (default: cpu)") + sp.add_argument("--dtype", default="bf16", choices=["bf16", "f16", "f32"], + help="Reference compute dtype (default: bf16, matches config)") + sp.add_argument("--torch-threads", type=int, default=1, + help="torch.set_num_threads (0 = unchanged)") + sp.add_argument("--max-new-tokens", type=int, default=256) + sp.add_argument("--enc-blocks", type=int, nargs="*", default=[], + help="Extra encoder block indices to capture") + sp.add_argument("--dec-blocks", type=int, nargs="*", default=[], + help="Extra decoder block indices to capture") + + +def main(argv: list[str]) -> int: + p = argparse.ArgumentParser(description="MOSS-Transcribe-Diarize reference dumper.") + sub = p.add_subparsers(dest="cmd", required=True) + + sp = sub.add_parser("decode", help="Run greedy decode + capture all dump points") + _add_common(sp) + sp.set_defaults(func=cmd_decode) + + # Single-pass decode captures enc.* + adaptor + dec.* in one forward, so + # `encoder` is an alias letting validate.py drive both stages uniformly. + ep = sub.add_parser("encoder", help="Alias for decode (full dump pass)") + _add_common(ep) + ep.set_defaults(func=cmd_decode) + + args = p.parse_args(argv[1:]) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/scripts/envs/moss/pyproject.toml b/scripts/envs/moss/pyproject.toml new file mode 100644 index 00000000..72e11179 --- /dev/null +++ b/scripts/envs/moss/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "transcribe-moss-env" +version = "0.1.0" +description = "Python env for MOSS-Transcribe-Diarize reference dumps and WER (trust_remote_code)" +requires-python = ">=3.11,<3.13" +dependencies = [ + "torch>=2.2", + "gguf>=0.10", + "huggingface-hub>=0.30", + "librosa>=0.10", + "numpy>=1.26", + "protobuf>=4.25", + "safetensors>=0.4", + "soundfile>=0.12", + "transformers==5.12.1", + "accelerate>=1.0", +] diff --git a/scripts/envs/moss/uv.lock b/scripts/envs/moss/uv.lock new file mode 100644 index 00000000..71b26c71 --- /dev/null +++ b/scripts/envs/moss/uv.lock @@ -0,0 +1,1274 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "accelerate" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, +] + +[[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.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +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 = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] + +[[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 = "filelock" +version = "3.29.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "gguf" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ae/17f1308ae45cd7b08ebb521747d5b23f4efc4d172038a4e228dd5106c3ff/gguf-0.19.0.tar.gz", hash = "sha256:dbadcd6cc7ccd44256f2229fe7c2dff5e8aa5cf0612ab987fd2b1a57e428923f", size = 111220, upload-time = "2026-05-06T13:04:03.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/bb/d71d6da82763528c2c2ed6b59a9d6142c6595545a4c448e2085d155e88c2/gguf-0.19.0-py3-none-any.whl", hash = "sha256:70bcd10edfe697fb2dad6e40af2234b9d8ece9a41a99761405121ebda1c3c1cd", size = 118475, upload-time = "2026-05-06T13:04:02.588Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163, upload-time = "2026-07-09T14:49:32.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336, upload-time = "2026-07-09T14:49:30.597Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "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 = "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", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "soundfile" }, + { name = "soxr" }, + { 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.48.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/55/595981f14fbae9ba966feb12af552b1fe69889e44e64ac883a731ed335e0/llvmlite-0.48.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:56a7e24607d3f02d7b1bae8d29c7e1e423d53143d68b072999777f19678fe77b", size = 40480651, upload-time = "2026-07-01T18:41:18.438Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/0109d1b9cb3f4603f3890e30bc66c65332b79185f12a045343b2ae431f67/llvmlite-0.48.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fa532d6bb3fd3f0803567c736401c54aecfe1a396d3ad25d2440d220e09f0e7", size = 59890118, upload-time = "2026-07-01T18:41:28.184Z" }, + { url = "https://files.pythonhosted.org/packages/02/eb/c5281be180c789cdffbf45b671884c57d7e61345ef3b0f643a4965e108e8/llvmlite-0.48.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979a66a3f28a02565383ff463527dce78e9b856298872a361283132488e83591", size = 58343458, upload-time = "2026-07-01T18:41:23.397Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f7/b3222b13f2d424dae3c9e63fde476af25ebccf1f3faf0b52d1b79fc15c70/llvmlite-0.48.0-cp311-cp311-win_amd64.whl", hash = "sha256:efaee0276e5e17c2b99b92e0c974bd484ef5977cf5dbc9168e82b71578edb47f", size = 41864734, upload-time = "2026-07-01T18:41:31.932Z" }, + { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, +] + +[[package]] +name = "narwhals" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/66ed1fc6e38a0c0f330627ec5c5d597990d6159b6712b82af0ad2c65f06c/narwhals-2.23.0.tar.gz", hash = "sha256:13e7ff5b4bb4a2f77b907c2e4d8a76e273dfc1323a3c997440a2f9fd26aed408", size = 656209, upload-time = "2026-07-01T11:21:53.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/4e/afc8c31605cb8be1d3bb4438c4d979daa104dab6306cd2b87abe9c3a7299/narwhals-2.23.0-py3-none-any.whl", hash = "sha256:769e7b9ab102c93d8fa019f6b4cd1a657909b04a20bf6210e5a35aae06814ae9", size = 458938, upload-time = "2026-07-01T11:21:51.677Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/02/970796b4daa709604cde22e87a7cda9bde473c278ea4a75f59fe38cee47f/numba-0.66.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bbd531c327557a9004507fa6bff06c53ab51a7a5776b75261bb9cef1efe2b2ea", size = 2727049, upload-time = "2026-07-01T23:12:11.296Z" }, + { url = "https://files.pythonhosted.org/packages/8c/99/33a6ed9c1a0b5e42efa98eb0edf617d61dca576c82625947377b1d4540c9/numba-0.66.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc6629becb21a867d85401ec89f426dd24c484a4193ade8a38309debfd1529ca", size = 3808870, upload-time = "2026-07-01T23:12:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/04/20/8c51126025211659235b8de2866dfa226984ae0c8273461a3cf374716741/numba-0.66.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac69f3ccb8af100f5913c1241edc9692bad1cdd2508721713f426eb06c9a659", size = 3514498, upload-time = "2026-07-01T23:12:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/9476940bc6d5caf5c0cf2e4c5feecbf01244bbe6f914614082dd7a3e520e/numba-0.66.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb601841d9e02e6237bb6522e36d0741614be3cfe2b482a6f00a41b5ba209443", size = 2780225, upload-time = "2026-07-01T23:12:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[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 = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/37/451aaddbf50922f34d744ad5ca919ae1fcfac112123885d9728f52a484b3/regex-2026.7.10.tar.gz", hash = "sha256:1050fedf0a8a92e843971120c2f57c3a99bea86c0dfa1d63a9fac053fe54b135", size = 416282, upload-time = "2026-07-10T19:49:46.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/16/bfd13770be1acd1c05506b93fc6be15c759d6417595d1ba334d355efbf26/regex-2026.7.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66d2c35587cd601c95965d5c0415058ba5cfd6ffbab7624ce198bd967102b341", size = 494639, upload-time = "2026-07-10T19:46:52.207Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/0086215709f0f705661f13ba81516287538886ef0d589c545c12b0484669/regex-2026.7.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28a0973eeffff4292f5a7ee498ab65d5e94ee8cc9cea364239251eb4a260a0f1", size = 295920, upload-time = "2026-07-10T19:46:53.63Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9e/8e07d0eea46d2cf36bf4d3794634bb0a820f016d31bc349dfef008d96b02/regex-2026.7.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8331484450b3894298bef8abecce532171ff6ac60b71f999eed10f2c01941a8a", size = 290673, upload-time = "2026-07-10T19:46:54.863Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/d83c446de21c70ff49d2f1b2ff2196ac79a4ac6373d2cfe496011a250600/regex-2026.7.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0639b2488b775a0109f55a5a2172deebdedb4b6c5ab0d48c90b43cbf5de58d17", size = 792378, upload-time = "2026-07-10T19:46:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0e/d265e0cc6da47aea97e90eb896be2d2e8f92d16add13bac04fa46a0fd972/regex-2026.7.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be4223af640d0aa04c05db81d5d96ada3ead9c09187d892fd37f4f97829480be", size = 861790, upload-time = "2026-07-10T19:46:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a5/62655f6208d1170a3e9188d6a45d4af0a5ae3b9da8b87d474818ac5ff016/regex-2026.7.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d3c75d57a00109255e60bc9c623b6ececaf7905eaab845c79f036670ed4750a2", size = 906530, upload-time = "2026-07-10T19:46:59.142Z" }, + { url = "https://files.pythonhosted.org/packages/86/b7/d65aa2e9ffb18677cd0afbcf5990da8519a4e50778deb1bca49f043c5174/regex-2026.7.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:724ee9379568658ec06362cf24325c5315cc5a67f61dfe585bfeff58300a355b", size = 799912, upload-time = "2026-07-10T19:47:00.534Z" }, + { url = "https://files.pythonhosted.org/packages/8f/19/3a5ce23ea2eb1fe36306aef49c79746ce297e4b434aeb981b525c661413a/regex-2026.7.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:732c19e5828eb287d01edb83b2eb87f283ba8e5fc3441c732709d3e8cbd14aaa", size = 773675, upload-time = "2026-07-10T19:47:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/fe/76/3c0eaa426700dd2ba14f2335f2b700a4e1484202254192ae440b83b8352a/regex-2026.7.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:982d07727c809b42a3968785354f11c3728414e4e90af0754345b431b2c32561", size = 781711, upload-time = "2026-07-10T19:47:03.425Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a8/a5a3fad84f9a7f897619f0f8e0a2c64946e9709044a186a8f869fb5c332f/regex-2026.7.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4574feca202f8c470bf678aed8b5d89df04aaf8dc677f3b83d92825051301c0f", size = 854539, upload-time = "2026-07-10T19:47:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c7/47e9b8c8ee77723b9eda74f517b6b25d2f555cf276c063a9eeea35bd86d5/regex-2026.7.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:80151ca5bfc6c4524186b3e08b499e97319b2001fc265ed2d4fc12c0d5692cdf", size = 763378, upload-time = "2026-07-10T19:47:06.845Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e27e42d9d42edf71205c7e6f5b2902bc874ea03c557c80da03b8ed16c9bf/regex-2026.7.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bb52e10e453b5493afe1f7702a2973bc10f4dd8901c0f2ed869ffaa3f8319296", size = 844663, upload-time = "2026-07-10T19:47:08.923Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b5/2423acb98362184ad9c8eebabafa15188d6a177daab919add8f2120fc6cd/regex-2026.7.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37aba1994d73b4944053ab65a15f313bd5c28c885dd7f0d494a11749d89db6e", size = 789236, upload-time = "2026-07-10T19:47:10.303Z" }, + { url = "https://files.pythonhosted.org/packages/60/ed/b387e84c8a3d6aa115dfb56865437a3fbaf28f4a6fb3b76cc6cce38ced70/regex-2026.7.10-cp311-cp311-win32.whl", hash = "sha256:6cbedeb5112f59dbd169385459b9943310bdd241c6966c19c5f6e2295055c93a", size = 266774, upload-time = "2026-07-10T19:47:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/f30a163a65ed1f07ad12c53af00a6bd2a7251a5329fba5a08adc6f9e81a3/regex-2026.7.10-cp311-cp311-win_amd64.whl", hash = "sha256:b1963ec5ba4d52788fb0eac6aca6eb8040e8e318c7e47ebbdfc09440c802919c", size = 277959, upload-time = "2026-07-10T19:47:13.231Z" }, + { url = "https://files.pythonhosted.org/packages/2b/de/61c8174171134cebb834ca9f8fe2ff8f49d8a3dd43453b48b537d0fbb49b/regex-2026.7.10-cp311-cp311-win_arm64.whl", hash = "sha256:3750c42d47712e362158a04d0fd80131f73a55e8c715b2885442a0ff6f9fc3fc", size = 276918, upload-time = "2026-07-10T19:47:14.693Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9c/2503d4ccf3452dc323f8baa3cf3ee10406037d52735c76cfced81423f183/regex-2026.7.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7252b48b0c60100095088fbeb281fca9a4fcf678a4e04b1c520c3f8613c952c4", size = 497114, upload-time = "2026-07-10T19:47:16.22Z" }, + { url = "https://files.pythonhosted.org/packages/91/eb/04534f4263a4f658cd20a511e9d6124350044f2214eb24fee2db96acf318/regex-2026.7.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:da6ef4cb8d457aab0482b50120136ae94238aaa421863eaa7d599759742c72d6", size = 297422, upload-time = "2026-07-10T19:47:17.794Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2d/35809de392ab66ba439b58c3187ae3b8b53c883233f284b59961e5725c99/regex-2026.7.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe7ff456c22725c9d9017f7a2a7df2b51af6df77314176760b22e2d05278e181", size = 292110, upload-time = "2026-07-10T19:47:19.188Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1e/5ce0fbe9aab071893ce2b7df020d0f561f7b411ec334124302468d587884/regex-2026.7.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3463a5f26be513a49e4d497debcf1b252a2db7b92c77d89621aa90b83d2dd38", size = 796800, upload-time = "2026-07-10T19:47:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/c1ccbada395c10e334763b583e1039b1660b142303ebb941d4269130b22f/regex-2026.7.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:948dfc62683a6947b9b486c4598d8f6e3ecc542478b6767b87d52be68aeb55c6", size = 865509, upload-time = "2026-07-10T19:47:22.135Z" }, + { url = "https://files.pythonhosted.org/packages/0e/06/f0b31afc16c1208f945b66290eb2a9936ab8becdfb23bbcedb91cc5f9d9b/regex-2026.7.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c2cbd385d82f63bb35edb60b09b08abad3619bd0a4a492ae59e55afaf98e1b9d", size = 912395, upload-time = "2026-07-10T19:47:24.128Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/8687de3a6c3220f4f872a9bf4bcd8dc249f2a96e7dddfa93de8bd4d16399/regex-2026.7.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6222cafe00e072bb2b8f14142cd969637411fbc4dd3b1d73a90a3b817fa046f", size = 801308, upload-time = "2026-07-10T19:47:25.696Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e3/60a40ec02a2315d826414a125640aceb6f30450574c530c8f352110ece0e/regex-2026.7.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:65ee5d1ac3cd541325f5ac92625b1c1505f4d171520dd931bda7952895c5321a", size = 777120, upload-time = "2026-07-10T19:47:27.158Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9a/ec579b4f840ac59bc7c192b56e66abd4cbf385615300d59f7c94bf6863ae/regex-2026.7.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa34473fbcc108fea403074f3f45091461b18b2047d136f16ffaa4c65ad46a68", size = 785164, upload-time = "2026-07-10T19:47:28.732Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1c/60d88afd5f98d4b0fb1f8b8969270628140dc01c7ff93a939f2aa83f31a6/regex-2026.7.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d028d189d8f38d7ff292f22187c0df37f2317f554d2ed9a2908ada330af57c0", size = 860161, upload-time = "2026-07-10T19:47:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/08ae3ba45fe79e48c9a888a3389a7ee7e2d8c580d2d996da5ece02dfdcb9/regex-2026.7.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:396ea70e4ea1f19571940add3bad9fd3eb6a19dc610d0d01f692bc1ba0c10cb4", size = 765829, upload-time = "2026-07-10T19:47:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/e6/e613c6755d19aca9d977cdc3418a1991ffc8f386779752dd8fdfa888ea89/regex-2026.7.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ebbf0d83ed5271991d666e54bb6c90ac2c55fb2ef3a88740c6af85dc85de2402", size = 852170, upload-time = "2026-07-10T19:47:33.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/89072f2060e6b844b4916d5bc40ef01e973640c703025707869264ec75ab/regex-2026.7.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a4571b2a093f6f6ee4fd281faa8ebf645abcf575f758173ea2605c7a1e1ecb", size = 789550, upload-time = "2026-07-10T19:47:35.395Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/4bc8be9a155035e63780ccac1da101f36194946fdc3f6fce90c7179fc6df/regex-2026.7.10-cp312-cp312-win32.whl", hash = "sha256:eac1207936555aa691ce32df1432b478f2729d54e6d93a1f4db9215bcd8eb47d", size = 267151, upload-time = "2026-07-10T19:47:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/9f5aade65bb98cc6e99c336e45a49a658300720c16721f3e687f8d754fec/regex-2026.7.10-cp312-cp312-win_amd64.whl", hash = "sha256:ecae626449d00db8c08f8f1fc00047a32d6d7eb5402b3976f5c3fda2b80a7a4f", size = 277751, upload-time = "2026-07-10T19:47:38.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/6f/d069dd12872ea1d50e17319d342f89e2072cae4b62f4245009a1108c74d8/regex-2026.7.10-cp312-cp312-win_arm64.whl", hash = "sha256:87794549a3f5c1c2bdfba2380c1bf87b931e375f4133d929da44f95e396bf5fe", size = 277063, upload-time = "2026-07-10T19:47:40.023Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "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", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { 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" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "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" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "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 = "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 = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, + { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, +] + +[[package]] +name = "transcribe-moss-env" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "accelerate" }, + { name = "gguf" }, + { name = "huggingface-hub" }, + { name = "librosa" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "safetensors" }, + { name = "soundfile" }, + { name = "torch" }, + { name = "transformers" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", specifier = ">=1.0" }, + { name = "gguf", specifier = ">=0.10" }, + { name = "huggingface-hub", specifier = ">=0.30" }, + { name = "librosa", specifier = ">=0.10" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "protobuf", specifier = ">=4.25" }, + { name = "safetensors", specifier = ">=0.4" }, + { name = "soundfile", specifier = ">=0.12" }, + { name = "torch", specifier = ">=2.2" }, + { name = "transformers", specifier = "==5.12.1" }, +] + +[[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.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/scripts/hf_cards/moss-transcribe-diarize.yaml b/scripts/hf_cards/moss-transcribe-diarize.yaml new file mode 100644 index 00000000..bd7b9682 --- /dev/null +++ b/scripts/hf_cards/moss-transcribe-diarize.yaml @@ -0,0 +1,109 @@ +# Spec for the HF README of handy-computer/MOSS-Transcribe-Diarize-gguf. +# Consumed by scripts/hf_cards/generate.py. + +hf_repo: OpenMOSS-Team/MOSS-Transcribe-Diarize +target_repo: handy-computer/MOSS-Transcribe-Diarize-gguf +transcribe_docs_url: https://github.com/handy-computer/transcribe.cpp/blob/main/docs/models/moss-transcribe-diarize.md + +upstream_commit: d7231bb +pin_date: 2026-07-12 + +validation: + reference: MOSS author repo (OpenMOSS/MOSS-Transcribe-Diarize) + commit: 3f5e15c + date: 2026-07-12 + +license: apache-2.0 +license_display: Apache-2.0 +pipeline_tag: automatic-speech-recognition +languages: + - en + - zh +tags: + - gguf + - transcribe.cpp + - asr + - speech-to-text + - moss + - audio-llm + - whisper-encoder + - qwen3 + - diarization + +summary: | + Offline English/Chinese speech-to-text with speaker diarization. A 0.9B + audio-LLM: a Whisper-Medium encoder (24 layers, d_model=1024) feeds a + 4x temporal merge + VQAdaptor bridge into a Qwen3-0.6B decoder (28 layers) + via audio-token injection. Takes a 16 kHz mono WAV and emits transcript + text in the canonical diarized format `[start][Sxx]text[end]`, where the + speaker tags and segment timestamps are generated text, not special + tokens. Not a streaming model. + +default_quant_index: 2 # Q8_0 + +# Capability flags for the transcribe_cpp metadata block. +capabilities: + streaming: false + translate: false + lang_detect: false + timestamps: segment # none | segment | word | token + +# Speedup-over-realtime (xRT) per rig/backend, from docs/models; published +# raw as rtf_ in the metadata block. Values average the Q8_0 jfk and +# dots publication cells. +perf: + m4-max: + metal: 28.1 + cpu: 5.8 + ryzen-4750u: + vulkan: 3.0 + cpu: 1.6 + +wer: + source: LibriSpeech test-clean + notes: | + WER measured on the full LibriSpeech `test-clean` split (2620 English + utterances) with the Whisper-style English text normalizer and jiwer + 3.x. MOSS emits the diarized format `[start][Sxx]text[end]`; the bracket + spans are metadata and are de-diarized to a space (for both hypothesis + and reference) before scoring, matching the author-repo reference runner. + These values describe this dataset only, not a general quality ranking: a + quant that scores slightly better here is not necessarily better in + real-world use, because dataset-specific decoding near-ties can make + quantization noise help or hurt individual utterances. The same-manifest + MOSS author-repo reference (bf16, greedy) lands at + **2.07%** with 95% bootstrap CI [1.82%, 2.40%]. The BF16 port lands at + 2.08% (within +0.01 of the reference, well inside the CI band); the + lower-bit presets sit between 1.93% and 1.99% (statistical noise) except + Q4_K_M at 2.59%, whose excess is a handful of 4-bit tail failures + (6 empty outputs, 5 English->Chinese language-drift utterances, 1 + timestamp-token repetition loop) rather than broad degradation. Prefer + Q5_K_M or higher if those tail failures matter. Reproduce with + `scripts/wer/run.py` + `scripts/wer/score.py --dediarize`; the runtime + output itself remains in the raw diarized format. + +quants: + - name: BF16 + filename: MOSS-Transcribe-Diarize-BF16.gguf + size: 1.83 GB + wer: 2.08% + - name: F16 + filename: MOSS-Transcribe-Diarize-F16.gguf + size: 1.83 GB + wer: 2.07% + - name: Q8_0 + filename: MOSS-Transcribe-Diarize-Q8_0.gguf + size: 987 MB + wer: 1.93% + - name: Q6_K + filename: MOSS-Transcribe-Diarize-Q6_K.gguf + size: 768 MB + wer: 1.96% + - name: Q5_K_M + filename: MOSS-Transcribe-Diarize-Q5_K_M.gguf + size: 700 MB + wer: 1.99% + - name: Q4_K_M + filename: MOSS-Transcribe-Diarize-Q4_K_M.gguf + size: 617 MB + wer: 2.59% diff --git a/scripts/validate.py b/scripts/validate.py index 32baa139..a5987261 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -160,14 +160,24 @@ def case_transcript_compare(manifest: dict[str, Any], case) -> str: if value is None: value = "exact" mode = str(value).lower() - if mode not in {"exact", "normalized"}: + if mode not in {"exact", "normalized", "dediarized"}: raise SystemExit( f"error: unsupported transcript_compare={value!r}; " - "expected 'exact' or 'normalized'" + "expected 'exact', 'normalized', or 'dediarized'" ) return mode +def dediarize_text(text: str) -> str: + """Strip inline [start]/[Sxx]/[end] bracket spans from an emergent-diarization + transcript, then normalize. The reference `text` field is already de-diarized, + so this is a no-op there; the C++ runtime returns the raw diarized transcript, + so bracket spans (whose numeric timestamps carry bf16-vs-f32 digit jitter) are + removed before comparison. Mirrors the WER harness de-diarization.""" + stripped = re.sub(r"\[[^\]]*\]", " ", text) + return normalize_text_for_compare(stripped) + + def find_gguf(repo: Path, family: str, slug: str | None = None, variant: str | None = None) -> Path: """Find a GGUF under models/. @@ -613,6 +623,9 @@ def cmd_compare(args: argparse.Namespace) -> int: if transcript_compare == "exact": ref_compare = ref_text cpp_compare = cpp_text + elif transcript_compare == "dediarized": + ref_compare = dediarize_text(ref_text) + cpp_compare = dediarize_text(cpp_text) else: ref_compare = normalize_text_for_compare(ref_text) cpp_compare = normalize_text_for_compare(cpp_text) diff --git a/scripts/wer/run_reference_moss_author.py b/scripts/wer/run_reference_moss_author.py new file mode 100644 index 00000000..5dfcbdec --- /dev/null +++ b/scripts/wer/run_reference_moss_author.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +run_reference_moss_author.py — MOSS-Transcribe-Diarize author-repo batch transcribe. + +Loads MOSS-Transcribe-Diarize once via transformers trust_remote_code and runs +greedy decode over a WER manifest. Writes run.py-compatible JSONL so +scripts/wer/score.py can score it the same way it scores the C++ port. + +The model always emits the canonical diarized+timestamped format +`[start][Sxx]text[end]`. For plain WER/CER scoring this runner de-diarizes the +hypothesis: every `[...]` bracket span (timestamps and speaker labels) is +metadata and is stripped, leaving the transcription text. This mirrors the +de-diarization the C++ WER path must apply. + +Usage (from repo root): + + uv run --project scripts/envs/moss \\ + scripts/wer/run_reference_moss_author.py \\ + --model OpenMOSS-Team/MOSS-Transcribe-Diarize \\ + --revision d7231bbae2587a4af278735eb765b318c4f64edd \\ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \\ + --out reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl + +Pass --device mps on Apple Silicon for a large speedup over CPU. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +import time +from pathlib import Path + + +DEFAULT_PROMPT = ( + "请将音频转写为文本,每一段需以起始时间戳和说话人编号" + "([S01]、[S02]、[S03]…)开头,正文为对应的语音内容," + "并在段末标注结束时间戳,以清晰标明该段语音范围。" +) + + +def dediarize(raw: str) -> str: + return " ".join(re.sub(r"\[[^\]]*\]", " ", raw).split()) + + +def main() -> int: + p = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--manifest", type=Path, required=True, + help="Input manifest JSONL (id/audio/ref_text/language).") + p.add_argument("--out", type=Path, required=True, + help="Output JSONL path (run.py-compatible).") + p.add_argument("--model", required=True, + help="HF repo id (OpenMOSS-Team/MOSS-Transcribe-Diarize) or local dir.") + p.add_argument("--revision", default=None, + help="HF revision to pin (ignored for local paths).") + p.add_argument("--device", default="cpu", + help="torch device (default: cpu; pass 'mps' on Apple).") + p.add_argument("--dtype", default="bf16", choices=["bf16", "f16", "f32"], + help="Compute dtype (default: bf16, matches config).") + p.add_argument("--torch-threads", type=int, default=0, + help="torch.set_num_threads (0 = unchanged).") + p.add_argument("--max-new-tokens", type=int, default=1024) + p.add_argument("--limit", type=int, default=0, + help="Process only the first N utterances (0 = all).") + args = p.parse_args() + + if not args.manifest.exists(): + print(f"error: manifest not found: {args.manifest}", file=sys.stderr) + return 2 + args.out.parent.mkdir(parents=True, exist_ok=True) + + import torch + if args.torch_threads > 0: + torch.set_num_threads(args.torch_threads) + + import soundfile as sf + import transformers + from transformers import AutoModelForCausalLM, AutoProcessor + + dtype = {"bf16": torch.bfloat16, "f16": torch.float16, "f32": torch.float32}[args.dtype] + print(f"loading: {args.model} (transformers {transformers.__version__}, " + f"device={args.device}, dtype={args.dtype})") + t0 = time.monotonic() + local_only = Path(args.model).is_dir() + revision = args.revision if not local_only else None + processor = AutoProcessor.from_pretrained( + args.model, revision=revision, + trust_remote_code=True, local_files_only=local_only, + ) + model = AutoModelForCausalLM.from_pretrained( + args.model, revision=revision, + trust_remote_code=True, local_files_only=local_only, + dtype=dtype, attn_implementation="eager", + ).eval().to(args.device) + load_ms = (time.monotonic() - t0) * 1000 + + # Build the prompt text once (audio value is irrelevant to template render; + # the pcm is passed per-utterance to processor(audio=...)). + messages = [ + { + "role": "user", + "content": [ + {"type": "audio", "audio": ""}, + {"type": "text", "text": DEFAULT_PROMPT}, + ], + } + ] + prompt_text = processor.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + + with open(args.manifest) as f: + manifest = [json.loads(line) for line in f if line.strip()] + if args.limit > 0: + manifest = manifest[:args.limit] + total = len(manifest) + print(f"manifest: {args.manifest} ({total} utterances)") + print(f"output: {args.out}") + + n_done = n_errors = 0 + t_loop = time.monotonic() + + with open(args.out, "w") as fout: + fout.write(json.dumps({ + "type": "batch_header", + "load_ms": round(load_ms, 1), + "framework": "moss_author", + "model": args.model, + "prompt": "DEFAULT_PROMPT (timestamp+diarize, zh)", + "dediarized": True, + }) + "\n") + fout.flush() + + for entry in manifest: + uid = entry["id"] + audio_path = entry["audio"] + ref_text = entry.get("ref_text", "") + + t_start = time.monotonic() + err = hyp_text = "" + try: + pcm, sr = sf.read(audio_path, dtype="float32") + if pcm.ndim > 1: + pcm = pcm[:, 0] + inputs = processor(text=prompt_text, audio=[pcm], return_tensors="pt") + inputs = inputs.to(args.device) + with torch.inference_mode(): + gen = model.generate( + input_ids=inputs["input_ids"], + attention_mask=inputs["attention_mask"], + input_features=inputs["input_features"], + audio_feature_lengths=inputs["audio_feature_lengths"], + audio_chunk_mapping=inputs["audio_chunk_mapping"], + max_new_tokens=args.max_new_tokens, + do_sample=False, num_beams=1, + ) + prompt_len = int(inputs["attention_mask"][0].sum().item()) + gen_ids = gen[0].detach().cpu().tolist()[prompt_len:] + eos_id = processor.tokenizer.eos_token_id + if eos_id in gen_ids: + gen_ids = gen_ids[:gen_ids.index(eos_id)] + raw_text = processor.tokenizer.decode( + gen_ids, skip_special_tokens=True).strip() + hyp_text = dediarize(raw_text) + except Exception as e: + err = f"{type(e).__name__}: {e}" + n_errors += 1 + + elapsed_ms = round((time.monotonic() - t_start) * 1000, 1) + fout.write(json.dumps({ + "id": uid, + "ref_text": ref_text, + "hyp_text": hyp_text.strip(), + "mel_ms": 0, "encode_ms": 0, + "decode_ms": elapsed_ms, "latency_ms": elapsed_ms, + "error": err, + }, ensure_ascii=False) + "\n") + fout.flush() + n_done += 1 + + if n_done % 25 == 0 or n_done == total: + wall = time.monotonic() - t_loop + rate = n_done / wall if wall > 0 else 0 + eta = (total - n_done) / rate if rate > 0 else 0 + print(f" [{n_done}/{total}] {rate:.2f} utt/s, " + f"ETA {eta/60:.1f} min, errors={n_errors}", flush=True) + + wall = time.monotonic() - t_loop + print(f"\ndone. {n_done} utterances in {wall:.1f}s " + f"({n_done/wall:.2f} utt/s), {n_errors} errors") + print(f"report: {args.out}") + return 0 if n_errors == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/wer/score.py b/scripts/wer/score.py index 21b68359..7061c3aa 100644 --- a/scripts/wer/score.py +++ b/scripts/wer/score.py @@ -19,6 +19,7 @@ Usage: uv run scripts/wer/score.py reports/wer/<...>.jsonl + uv run scripts/wer/score.py reports/wer/<...>.jsonl --dediarize uv run scripts/wer/score.py reports/wer/<...>.jsonl --language vi uv run scripts/wer/score.py reports/wer/<...>.jsonl --language zh --metric cer @@ -37,6 +38,7 @@ import argparse import json import random +import re import sys from pathlib import Path from typing import Callable @@ -51,6 +53,21 @@ CER_LANGUAGES = {"zh", "yue", "ja", "ko", "th"} +# Optional diarization metadata spans: timestamps `[6.98]`, speaker tags +# `[S01]`, and annotations such as `[laughter]` / ``. Whether these count +# as transcription content is dataset-specific, so removal is opt-in. +_METADATA_SPAN = re.compile(r"[\[<][^\]>]*[\]>]") + + +def dediarize(text: str | None) -> str: + """Replace bracketed metadata spans with a space, then collapse runs. + + Diarizing models such as MOSS emit `[start][Sxx]text[end]`. Replacing the + spans with spaces avoids fusing words on either side during normalization. + """ + return " ".join(_METADATA_SPAN.sub(" ", text or "").split()) + + def base_language(lang: str | None) -> str | None: if not lang: return None @@ -119,6 +136,9 @@ def main() -> int: default="auto", help="Error metric. 'auto' picks CER for " "zh/yue/ja/ko/th and WER otherwise (default: auto)") + p.add_argument("--dediarize", action="store_true", + help="Remove bracketed timestamps, speaker labels, and " + "annotations before normalization (opt-in)") args = p.parse_args() if not args.report.exists(): @@ -165,9 +185,13 @@ def main() -> int: print("error: report is empty", file=sys.stderr) return 2 - # Normalize refs and hyps. - refs_raw = [e["ref_text"] for e in entries] - hyps_raw = [e["hyp_text"] for e in entries] + # Normalize refs and hyps. Diarization metadata removal is opt-in because + # bracketed annotations may be scoring content in other datasets. + refs_raw = [e["ref_text"] for e in entries] + hyps_raw = [e["hyp_text"] for e in entries] + if args.dediarize: + refs_raw = [dediarize(r) for r in refs_raw] + hyps_raw = [dediarize(h) for h in hyps_raw] refs_norm = [normalizer(r) for r in refs_raw] hyps_norm = [normalizer(h) for h in hyps_raw] @@ -245,6 +269,7 @@ def main() -> int: score = { "metric": metric, "language": language, + "dediarized": args.dediarize, "error_rate": round(global_rate, 6), "error_rate_pct": round(global_rate * 100, 2), "error_rate_ci_lo": round(ci_lo, 6), diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3a2753b1..99da9f39 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,11 @@ add_library(transcribe arch/qwen3_asr/weights.cpp arch/qwen3_asr/encoder.cpp arch/qwen3_asr/decoder.cpp + arch/moss/model.cpp + arch/moss/capabilities.cpp + arch/moss/weights.cpp + arch/moss/encoder.cpp + arch/moss/decoder.cpp arch/voxtral/model.cpp arch/voxtral/capabilities.cpp arch/voxtral/weights.cpp diff --git a/src/arch/moss/capabilities.cpp b/src/arch/moss/capabilities.cpp new file mode 100644 index 00000000..b69b6a03 --- /dev/null +++ b/src/arch/moss/capabilities.cpp @@ -0,0 +1,23 @@ +// arch/moss/capabilities.cpp - family invariants. + +#include "moss.h" + +namespace transcribe::moss { + +void apply_family_invariants(transcribe_model & model) { + transcribe_capabilities & caps = model.caps; + + caps.native_sample_rate = 16000; + + // Segment timestamps and speaker tags are emergent plain text inside the + // transcript ([start][Sxx]text[end]); the runtime returns the raw string + // without parsing them into structured segments/timestamps. + caps.max_timestamp_kind = TRANSCRIBE_TIMESTAMPS_NONE; + + // Transcription only; language is steered by the fixed prompt (no token). + caps.supports_translate = false; + + transcribe::set_feature(&model, TRANSCRIBE_FEATURE_CANCELLATION, true); +} + +} // namespace transcribe::moss diff --git a/src/arch/moss/decoder.cpp b/src/arch/moss/decoder.cpp new file mode 100644 index 00000000..3a7b3dec --- /dev/null +++ b/src/arch/moss/decoder.cpp @@ -0,0 +1,390 @@ +// arch/moss/decoder.cpp - MOSS Qwen3 LM prefill/step graph builders. +// +// Reference: Qwen3Model as composed by modeling_moss_transcribe_diarize.py. The +// per-block math is shared with arch/qwen3_asr via src/causal_lm/. This file +// owns graph allocation, the blend audio injection, tensor naming + dump +// points, the final RMSNorm + tied lm_head, and the driver loops. + +#include "decoder.h" + +#include "causal_lm/causal_lm.h" +#include "ggml.h" +#include "transcribe-debug.h" +#include "transcribe-log.h" + +#include + +namespace transcribe::moss { + +namespace { + +ggml_tensor * named(ggml_tensor * t, const char * name) { + if (t != nullptr && name != nullptr) { + ggml_set_name(t, name); + } + return t; +} + +causal_lm::BlockView to_block_view(const MossDecBlock & b) { + causal_lm::BlockView v{}; + v.norm_attn_w = b.norm_attn_w; + v.norm_ffn_w = b.norm_ffn_w; + v.attn_q_w = b.attn_q_w; + v.attn_k_w = b.attn_k_w; + v.attn_v_w = b.attn_v_w; + v.attn_o_w = b.attn_o_w; + v.attn_q_norm = b.attn_q_norm; + v.attn_k_norm = b.attn_k_norm; + v.ffn_gate_up_w = b.ffn_gate_up_w; + v.ffn_down_w = b.ffn_down_w; + return v; +} + +causal_lm::BlockParams to_block_params(const MossHParams & hp) { + causal_lm::BlockParams p{}; + p.n_heads = hp.dec_n_heads; + p.n_kv_heads = hp.dec_n_kv_heads; + p.head_dim = hp.dec_head_dim; + p.max_position = hp.dec_max_position_embeddings; + p.rms_eps = hp.dec_rms_norm_eps; + p.rope_theta = hp.dec_rope_theta; + return p; +} + +} // namespace + +PrefillBuild build_prefill_graph(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int T_prompt, + bool use_flash, + bool slice_last) { + PrefillBuild pb{}; + pb.T_prompt = T_prompt; + if (ctx == nullptr || T_prompt <= 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss decoder: invalid T_prompt=%d", T_prompt); + return pb; + } + if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss decoder: kv_cache not initialized"); + return pb; + } + if (T_prompt > kv_cache.n_ctx) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss decoder: T_prompt=%d exceeds n_ctx=%d", T_prompt, kv_cache.n_ctx); + return pb; + } + + const int64_t hidden = hp.dec_hidden; + const int64_t vocab = hp.dec_vocab_size; + const int n_layer = hp.dec_n_layers; + const float rms_eps = hp.dec_rms_norm_eps; + const auto bp = to_block_params(hp); + + pb.input_ids_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, T_prompt); + named(pb.input_ids_in, "dec.input_ids"); + ggml_set_input(pb.input_ids_in); + + pb.audio_dense_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, T_prompt); + named(pb.audio_dense_in, "dec.audio_dense"); + ggml_set_input(pb.audio_dense_in); + + pb.keep_mask_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 1, T_prompt); + named(pb.keep_mask_in, "dec.keep_mask"); + ggml_set_input(pb.keep_mask_in); + + pb.positions_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, T_prompt); + named(pb.positions_in, "dec.positions"); + ggml_set_input(pb.positions_in); + + pb.mask_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F16, T_prompt, T_prompt); + named(pb.mask_in, "dec.attn_mask"); + ggml_set_input(pb.mask_in); + + ggml_cgraph * gf = ggml_new_graph_custom(ctx, 16384, false); + if (gf == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss decoder: ggml_new_graph_custom failed"); + return pb; + } + pb.graph = gf; + + ggml_tensor * token_emb = ggml_get_rows(ctx, weights.dec_embed.token_w, pb.input_ids_in); + named(token_emb, "dec.token_emb"); + pb.dumps.token_emb = token_emb; + transcribe::debug::mark_tensor_for_dump(token_emb); + + // Blend injection: x = token_emb * keep_mask + audio_dense. + ggml_tensor * x = ggml_add(ctx, ggml_mul(ctx, token_emb, pb.keep_mask_in), pb.audio_dense_in); + named(x, "dec.audio_injected"); + pb.dumps.audio_injected = x; + transcribe::debug::mark_tensor_for_dump(x); + + for (int il = 0; il < n_layer; ++il) { + causal_lm::BlockOpts opts{}; + opts.use_flash = use_flash; + opts.slice_last_before_ffn = slice_last && (il == n_layer - 1); + x = causal_lm::block_prefill(ctx, gf, x, to_block_view(weights.dec_blocks[il]), bp, kv_cache, il, T_prompt, + pb.mask_in, pb.positions_in, opts); + if (il == 0) { + named(x, "dec.block.0.out"); + pb.dumps.block_0_out = x; + transcribe::debug::mark_tensor_for_dump(x); + } else if (il == n_layer - 1) { + char nm[64]; + std::snprintf(nm, sizeof(nm), "dec.block.%d.out", il); + named(x, nm); + pb.dumps.block_last_out = x; + transcribe::debug::mark_tensor_for_dump(x); + } + } + + x = ggml_mul(ctx, ggml_rms_norm(ctx, x, rms_eps), weights.dec_final.norm_w); + named(x, "dec.out_before_head"); + pb.dumps.out_before_head = x; + transcribe::debug::mark_tensor_for_dump(x); + + ggml_tensor * last_x; + if (slice_last) { + last_x = x; + } else { + last_x = ggml_view_2d(ctx, x, hidden, 1, ggml_element_size(x) * hidden, + ggml_element_size(x) * hidden * static_cast(T_prompt - 1)); + last_x = ggml_cont(ctx, last_x); + } + + ggml_tensor * logits = ggml_mul_mat(ctx, weights.dec_embed.token_w, last_x); + logits = ggml_reshape_1d(ctx, logits, vocab); + named(logits, "dec.logits_raw"); + pb.dumps.logits_raw = logits; + transcribe::debug::mark_tensor_for_dump(logits); + + pb.out = logits; + ggml_set_output(pb.out); + ggml_build_forward_expand(gf, pb.out); + if (pb.dumps.token_emb) { + ggml_build_forward_expand(gf, pb.dumps.token_emb); + } + if (pb.dumps.audio_injected) { + ggml_build_forward_expand(gf, pb.dumps.audio_injected); + } + if (pb.dumps.block_0_out) { + ggml_build_forward_expand(gf, pb.dumps.block_0_out); + } + if (pb.dumps.block_last_out) { + ggml_build_forward_expand(gf, pb.dumps.block_last_out); + } + if (pb.dumps.out_before_head) { + ggml_build_forward_expand(gf, pb.dumps.out_before_head); + } + return pb; +} + +StepBuild build_step_graph(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int max_n_kv, + bool use_flash) { + StepBuild sb{}; + sb.max_n_kv = max_n_kv; + if (ctx == nullptr || max_n_kv <= 0 || kv_cache.self_k == nullptr || max_n_kv > kv_cache.n_ctx) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss step: invalid arg (max_n_kv=%d)", max_n_kv); + return sb; + } + + const int64_t vocab = hp.dec_vocab_size; + const int n_layer = hp.dec_n_layers; + const float rms_eps = hp.dec_rms_norm_eps; + const auto bp = to_block_params(hp); + + sb.input_id_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, 1); + ggml_set_name(sb.input_id_in, "step.input_id"); + ggml_set_input(sb.input_id_in); + sb.position_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, 1); + ggml_set_name(sb.position_in, "step.position"); + ggml_set_input(sb.position_in); + sb.kv_idx_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I64, 1); + ggml_set_name(sb.kv_idx_in, "step.kv_idx"); + ggml_set_input(sb.kv_idx_in); + sb.mask_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F16, max_n_kv, 1); + ggml_set_name(sb.mask_in, "step.mask"); + ggml_set_input(sb.mask_in); + + ggml_cgraph * gf = ggml_new_graph_custom(ctx, 8192, false); + if (gf == nullptr) { + return sb; + } + sb.graph = gf; + + ggml_tensor * x = ggml_get_rows(ctx, weights.dec_embed.token_w, sb.input_id_in); + for (int il = 0; il < n_layer; ++il) { + x = causal_lm::block_step(ctx, gf, x, to_block_view(weights.dec_blocks[il]), bp, kv_cache, il, max_n_kv, + sb.mask_in, sb.position_in, sb.kv_idx_in, use_flash); + } + x = ggml_mul(ctx, ggml_rms_norm(ctx, x, rms_eps), weights.dec_final.norm_w); + ggml_tensor * logits = ggml_mul_mat(ctx, weights.dec_embed.token_w, x); + logits = ggml_reshape_1d(ctx, logits, vocab); + ggml_set_name(logits, "step.logits"); + sb.logits = logits; + transcribe::debug::mark_tensor_for_dump(logits); + + ggml_tensor * amax = ggml_argmax(ctx, logits); + ggml_set_name(amax, "step.argmax"); + sb.out = amax; + ggml_set_output(sb.out); + ggml_build_forward_expand(gf, sb.out); + ggml_build_forward_expand(gf, logits); + return sb; +} + +PrefillBuildBatched build_prefill_graph_batched(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int T_prompt_max, + int n_batch, + bool use_flash) { + PrefillBuildBatched pb{}; + pb.T_prompt_max = T_prompt_max; + pb.n_batch = n_batch; + if (ctx == nullptr || T_prompt_max <= 0 || n_batch <= 0 || !use_flash) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss prefill(batched): invalid arg (requires use_flash)"); + return pb; + } + if (kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt_max > kv_cache.n_ctx) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss prefill(batched): kv_cache mismatch"); + return pb; + } + + const int64_t hidden = hp.dec_hidden; + const int64_t vocab = hp.dec_vocab_size; + const int n_layer = hp.dec_n_layers; + const float rms_eps = hp.dec_rms_norm_eps; + const int B = n_batch; + const auto bp = to_block_params(hp); + + pb.input_ids_in = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, T_prompt_max, B); + ggml_set_name(pb.input_ids_in, "prefill.input_ids"); + ggml_set_input(pb.input_ids_in); + pb.audio_dense_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hidden, static_cast(T_prompt_max) * B); + ggml_set_name(pb.audio_dense_in, "prefill.audio_dense"); + ggml_set_input(pb.audio_dense_in); + pb.keep_mask_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 1, static_cast(T_prompt_max) * B); + ggml_set_name(pb.keep_mask_in, "prefill.keep_mask"); + ggml_set_input(pb.keep_mask_in); + pb.positions_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, T_prompt_max); + ggml_set_name(pb.positions_in, "prefill.positions"); + ggml_set_input(pb.positions_in); + pb.mask_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F16, T_prompt_max, T_prompt_max); + ggml_set_name(pb.mask_in, "prefill.mask"); + ggml_set_input(pb.mask_in); + pb.kv_idx_in = ggml_new_tensor_2d(ctx, GGML_TYPE_I64, T_prompt_max, B); + ggml_set_name(pb.kv_idx_in, "prefill.kv_idx"); + ggml_set_input(pb.kv_idx_in); + pb.last_idx_in = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, 1, B); + ggml_set_name(pb.last_idx_in, "prefill.last_idx"); + ggml_set_input(pb.last_idx_in); + + ggml_cgraph * gf = ggml_new_graph_custom(ctx, 16384, false); + if (gf == nullptr) { + return pb; + } + pb.graph = gf; + + ggml_tensor * ids_flat = ggml_reshape_1d(ctx, pb.input_ids_in, static_cast(T_prompt_max) * B); + ggml_tensor * x = ggml_get_rows(ctx, weights.dec_embed.token_w, ids_flat); + x = ggml_add(ctx, ggml_mul(ctx, x, pb.keep_mask_in), pb.audio_dense_in); + x = ggml_reshape_3d(ctx, x, hidden, T_prompt_max, B); + + for (int il = 0; il < n_layer; ++il) { + x = causal_lm::block_prefill_batched(ctx, gf, x, to_block_view(weights.dec_blocks[il]), bp, kv_cache, il, + T_prompt_max, B, pb.mask_in, pb.positions_in, pb.kv_idx_in, use_flash); + if (x == nullptr) { + pb.graph = nullptr; + return pb; + } + } + + ggml_tensor * x_last = ggml_get_rows(ctx, x, pb.last_idx_in); + x_last = ggml_reshape_2d(ctx, x_last, hidden, B); + x_last = ggml_mul(ctx, ggml_rms_norm(ctx, x_last, rms_eps), weights.dec_final.norm_w); + ggml_tensor * logits = ggml_mul_mat(ctx, weights.dec_embed.token_w, x_last); + logits = ggml_reshape_2d(ctx, logits, vocab, B); + ggml_set_name(logits, "prefill.logits"); + pb.logits = logits; + + ggml_tensor * amax = ggml_argmax(ctx, logits); + ggml_set_name(amax, "prefill.argmax"); + pb.out = amax; + ggml_set_output(pb.out); + ggml_build_forward_expand(gf, pb.out); + ggml_build_forward_expand(gf, logits); + return pb; +} + +StepBuildBatched build_step_graph_batched(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int max_n_kv, + int n_batch, + bool use_flash) { + StepBuildBatched sb{}; + sb.max_n_kv = max_n_kv; + sb.n_batch = n_batch; + if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || + kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss step(batched): invalid arg (requires use_flash)"); + return sb; + } + + const int64_t vocab = hp.dec_vocab_size; + const int n_layer = hp.dec_n_layers; + const float rms_eps = hp.dec_rms_norm_eps; + const int B = n_batch; + const auto bp = to_block_params(hp); + + sb.input_ids_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, B); + ggml_set_name(sb.input_ids_in, "step.input_ids"); + ggml_set_input(sb.input_ids_in); + sb.position_in = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, B); + ggml_set_name(sb.position_in, "step.position"); + ggml_set_input(sb.position_in); + sb.kv_idx_in = ggml_new_tensor_2d(ctx, GGML_TYPE_I64, 1, B); + ggml_set_name(sb.kv_idx_in, "step.kv_idx"); + ggml_set_input(sb.kv_idx_in); + sb.mask_in = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, max_n_kv, 1, 1, B); + ggml_set_name(sb.mask_in, "step.mask"); + ggml_set_input(sb.mask_in); + + ggml_cgraph * gf = ggml_new_graph_custom(ctx, 8192, false); + if (gf == nullptr) { + return sb; + } + sb.graph = gf; + + ggml_tensor * x = ggml_get_rows(ctx, weights.dec_embed.token_w, sb.input_ids_in); + for (int il = 0; il < n_layer; ++il) { + x = causal_lm::block_step_batched(ctx, gf, x, to_block_view(weights.dec_blocks[il]), bp, kv_cache, il, max_n_kv, + B, sb.mask_in, sb.position_in, sb.kv_idx_in, use_flash); + if (x == nullptr) { + sb.graph = nullptr; + return sb; + } + } + x = ggml_mul(ctx, ggml_rms_norm(ctx, x, rms_eps), weights.dec_final.norm_w); + ggml_tensor * logits = ggml_mul_mat(ctx, weights.dec_embed.token_w, x); + logits = ggml_reshape_2d(ctx, logits, vocab, B); + ggml_set_name(logits, "step.logits"); + sb.logits = logits; + + ggml_tensor * amax = ggml_argmax(ctx, logits); + ggml_set_name(amax, "step.argmax"); + sb.out = amax; + ggml_set_output(sb.out); + ggml_build_forward_expand(gf, sb.out); + ggml_build_forward_expand(gf, logits); + return sb; +} + +} // namespace transcribe::moss diff --git a/src/arch/moss/decoder.h b/src/arch/moss/decoder.h new file mode 100644 index 00000000..b06b8ed6 --- /dev/null +++ b/src/arch/moss/decoder.h @@ -0,0 +1,118 @@ +// arch/moss/decoder.h - MOSS Qwen3 LM graph builders (prefill + step). +// +// 28-layer Qwen3 causal decoder, identical block math to arch/qwen3_asr via +// src/causal_lm/ (pre-LN RMSNorm, GQA with per-head q/k RMSNorm, NeoX RoPE +// theta 1e6, SwiGLU on packed gate+up, tied lm_head). +// +// Audio injection differs from qwen3_asr: MOSS audio-pad positions are NOT +// contiguous (the processor interleaves time-marker digit tokens into the +// span), so both single and batched prefill use the elementwise blend +// x = token_emb * keep_mask + audio_dense +// where audio_dense holds the adaptor features scattered host-side into the +// audio-pad prompt positions (0 elsewhere) and keep_mask is 0 there / 1 else. + +#pragma once + +#include "causal_lm/causal_lm.h" +#include "ggml.h" +#include "weights.h" + +struct ggml_context; +struct ggml_cgraph; +struct ggml_tensor; + +namespace transcribe::moss { + +struct DecoderDumps { + ggml_tensor * token_emb = nullptr; + ggml_tensor * audio_injected = nullptr; + ggml_tensor * block_0_out = nullptr; + ggml_tensor * block_last_out = nullptr; + ggml_tensor * out_before_head = nullptr; + ggml_tensor * logits_raw = nullptr; +}; + +struct PrefillBuild { + ggml_tensor * input_ids_in = nullptr; // [T_prompt] i32 + ggml_tensor * audio_dense_in = nullptr; // [hidden, T_prompt] f32 + ggml_tensor * keep_mask_in = nullptr; // [1, T_prompt] f32 (0=audio, 1=keep) + ggml_tensor * positions_in = nullptr; // [T_prompt] i32 + ggml_tensor * mask_in = nullptr; // [T_prompt, T_prompt] f16 causal + ggml_tensor * out = nullptr; // [vocab] last-position logits + DecoderDumps dumps{}; + ggml_cgraph * graph = nullptr; + int T_prompt = 0; +}; + +PrefillBuild build_prefill_graph(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int T_prompt, + bool use_flash, + bool slice_last); + +struct StepBuild { + ggml_tensor * input_id_in = nullptr; // [1] i32 + ggml_tensor * position_in = nullptr; // [1] i32 + ggml_tensor * kv_idx_in = nullptr; // [1] i64 + ggml_tensor * mask_in = nullptr; // [max_n_kv, 1] f16 + ggml_tensor * out = nullptr; // [1] i32 argmax + ggml_tensor * logits = nullptr; // [vocab] (for gen-step dumps) + ggml_cgraph * graph = nullptr; + int max_n_kv = 0; +}; + +StepBuild build_step_graph(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int max_n_kv, + bool use_flash); + +// ---------- Batched (offline run_batch) ---------- + +struct PrefillBuildBatched { + ggml_tensor * input_ids_in = nullptr; // [T_prompt_max, B] i32 + ggml_tensor * audio_dense_in = nullptr; // [hidden, T_prompt_max*B] f32 + ggml_tensor * keep_mask_in = nullptr; // [1, T_prompt_max*B] f32 + ggml_tensor * positions_in = nullptr; // [T_prompt_max] i32 + ggml_tensor * mask_in = nullptr; // [T_prompt_max, T_prompt_max] f16 + ggml_tensor * kv_idx_in = nullptr; // [T_prompt_max, B] i64 + ggml_tensor * last_idx_in = nullptr; // [1, B] i32 + ggml_tensor * logits = nullptr; // [vocab, B] + ggml_tensor * out = nullptr; // [B] i32 argmax + ggml_cgraph * graph = nullptr; + int T_prompt_max = 0; + int n_batch = 0; +}; + +PrefillBuildBatched build_prefill_graph_batched(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int T_prompt_max, + int n_batch, + bool use_flash); + +struct StepBuildBatched { + ggml_tensor * input_ids_in = nullptr; // [B] i32 + ggml_tensor * position_in = nullptr; // [B] i32 + ggml_tensor * kv_idx_in = nullptr; // [1, B] i64 + ggml_tensor * mask_in = nullptr; // [max_n_kv, 1, 1, B] f16 + ggml_tensor * logits = nullptr; // [vocab, B] + ggml_tensor * out = nullptr; // [B] i32 + ggml_cgraph * graph = nullptr; + int max_n_kv = 0; + int n_batch = 0; +}; + +StepBuildBatched build_step_graph_batched(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + transcribe::causal_lm::KvCache & kv_cache, + int max_n_kv, + int n_batch, + bool use_flash); + +} // namespace transcribe::moss diff --git a/src/arch/moss/encoder.cpp b/src/arch/moss/encoder.cpp new file mode 100644 index 00000000..a5b4cf40 --- /dev/null +++ b/src/arch/moss/encoder.cpp @@ -0,0 +1,269 @@ +// arch/moss/encoder.cpp - MOSS Whisper encoder + VQAdaptor graph builders. +// +// The encoder is stock HF WhisperEncoder (see arch/whisper/encoder.cpp): a +// 2-layer Conv1d stem (GELU-erf) + learned positional embedding + N pre-LN +// transformer blocks (LayerNorm, gelu FFN; q/v/out carry bias, k does not) + +// final LayerNorm. The adaptor consumes the 4x-time-merged, per-chunk-trimmed, +// concatenated encoder output. + +#include "encoder.h" + +#include "conformer/conformer.h" +#include "ggml.h" +#include "transcribe-debug.h" +#include "transcribe-log.h" + +#include +#include + +namespace transcribe::moss { + +namespace { + +namespace conf = transcribe::conformer; +using conf::layer_norm; +using conf::named; + +// LayerNorm with an explicit eps (the VQAdaptor uses 1e-6, not conformer's 1e-5). +ggml_tensor * layer_norm_eps(ggml_context * ctx, ggml_tensor * x, ggml_tensor * gamma, ggml_tensor * beta, float eps) { + ggml_tensor * y = ggml_norm(ctx, x, eps); + y = ggml_mul(ctx, y, gamma); + if (beta != nullptr) { + y = ggml_add(ctx, y, beta); + } + return y; +} + +// Reshape a 1D conv bias [Cout] to [1, Cout, 1, 1] so it broadcasts across T +// against a ggml_conv_1d output [T_out, Cout, 1, 1]. +ggml_tensor * add_conv1d_bias(ggml_context * ctx, ggml_tensor * conv_out, ggml_tensor * bias_1d) { + if (bias_1d == nullptr) { + return conv_out; + } + const int64_t channels = bias_1d->ne[0]; + ggml_tensor * bias_4d = ggml_reshape_4d(ctx, bias_1d, 1, channels, 1, 1); + return ggml_add(ctx, conv_out, bias_4d); +} + +// Bidirectional multi-head self-attention. x: [d_model, T] -> [d_model, T]. +// Whisper quirk: q/v/out carry bias, k does not. +ggml_tensor * mha_encoder(ggml_context * ctx, + ggml_tensor * x, + const MossEncBlock & b, + int n_heads, + int d_model, + bool use_flash) { + const int head_dim = d_model / n_heads; + const float scale = 1.0f / std::sqrt(static_cast(head_dim)); + const int64_t T = x->ne[1]; + + ggml_tensor * q = ggml_mul_mat(ctx, b.attn_q_w, x); + q = ggml_add(ctx, q, b.attn_q_b); + ggml_tensor * k = ggml_mul_mat(ctx, b.attn_k_w, x); + ggml_tensor * v = ggml_mul_mat(ctx, b.attn_v_w, x); + v = ggml_add(ctx, v, b.attn_v_b); + + q = ggml_permute(ctx, ggml_reshape_3d(ctx, q, head_dim, n_heads, T), 0, 2, 1, 3); + k = ggml_permute(ctx, ggml_reshape_3d(ctx, k, head_dim, n_heads, T), 0, 2, 1, 3); + v = ggml_permute(ctx, ggml_reshape_3d(ctx, v, head_dim, n_heads, T), 0, 2, 1, 3); + + ggml_tensor * o; + if (use_flash) { + ggml_tensor * q_c = ggml_cont(ctx, q); + ggml_tensor * k_c = ggml_cont(ctx, k); + ggml_tensor * v_c = ggml_cont(ctx, v); + o = ggml_flash_attn_ext(ctx, q_c, k_c, v_c, nullptr, scale, 0.0f, 0.0f); + o = ggml_reshape_2d(ctx, o, d_model, T); + } else { + ggml_tensor * kq = ggml_mul_mat(ctx, ggml_cont(ctx, k), ggml_cont(ctx, q)); + ggml_tensor * kq_soft = ggml_soft_max_ext(ctx, kq, nullptr, scale, 0.0f); + ggml_tensor * v_t = ggml_cont(ctx, ggml_permute(ctx, v, 1, 0, 2, 3)); + o = ggml_mul_mat(ctx, v_t, kq_soft); + o = ggml_cont(ctx, ggml_permute(ctx, o, 0, 2, 1, 3)); + o = ggml_reshape_2d(ctx, o, d_model, T); + } + + o = ggml_mul_mat(ctx, b.attn_out_w, o); + o = ggml_add(ctx, o, b.attn_out_b); + return o; +} + +// y = x + MHSA(LN(x)); y = y + FFN(LN(y)). FFN: fc2(GELU-erf(fc1(x))). +ggml_tensor * build_block(ggml_context * ctx, + ggml_tensor * x, + const MossEncBlock & b, + int n_heads, + int d_model, + bool use_flash) { + { + ggml_tensor * y = layer_norm(ctx, x, b.norm_attn_w, b.norm_attn_b); + y = mha_encoder(ctx, y, b, n_heads, d_model, use_flash); + x = ggml_add(ctx, x, y); + } + { + ggml_tensor * y = layer_norm(ctx, x, b.norm_ffn_w, b.norm_ffn_b); + y = ggml_mul_mat(ctx, b.ffn_fc1_w, y); + y = ggml_add(ctx, y, b.ffn_fc1_b); + y = ggml_gelu_erf(ctx, y); + y = ggml_mul_mat(ctx, b.ffn_fc2_w, y); + y = ggml_add(ctx, y, b.ffn_fc2_b); + x = ggml_add(ctx, x, y); + } + return x; +} + +} // namespace + +int whisper_enc_len(int n_mel_frames) { + return n_mel_frames / 2; +} + +int audio_token_length(int num_samples, const MossHParams & hp) { + const int stride = hp.fe_hop_length * 2 * hp.audio_merge_size; + if (stride <= 0 || num_samples <= 0) { + return 0; + } + return (num_samples - 1) / stride + 1; +} + +EncoderBuild build_encoder_graph(ggml_context * ctx, + const MossWeights & w, + const MossHParams & hp, + int n_mel_frames, + bool use_flash) { + EncoderBuild eb{}; + if (ctx == nullptr || n_mel_frames <= 0 || n_mel_frames % 2 != 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encoder: invalid n_mel_frames=%d", n_mel_frames); + return eb; + } + + const int d_model = hp.enc_d_model; + const int n_mels = hp.enc_num_mel_bins; + const int n_heads = hp.enc_n_heads; + const int T_enc = n_mel_frames / 2; + if (T_enc > hp.enc_max_source_positions) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encoder: T_enc=%d exceeds max_source_positions=%d", T_enc, + hp.enc_max_source_positions); + return eb; + } + + // mel_in ne=[T, n_mels]: MelFrontend emits row-major [num_mels, n_frames] + // (t-fastest), which IS ggml [T, n_mels] — and exactly the [W, IC] layout + // ggml_conv_1d wants, so no leading transpose (cf. qwen3_asr). + eb.mel_in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, n_mel_frames, n_mels); + named(eb.mel_in, "enc.mel.in"); + ggml_set_input(eb.mel_in); + + // conv stem. + ggml_tensor * x = conf::conv_1d_f32(ctx, w.enc_stem.conv0_w, eb.mel_in, 1, 1, 1); // [T, d_model] + x = add_conv1d_bias(ctx, x, w.enc_stem.conv0_b); + x = ggml_gelu_erf(ctx, x); + + x = conf::conv_1d_f32(ctx, w.enc_stem.conv1_w, x, 2, 1, 1); // [T_enc, d_model] + x = add_conv1d_bias(ctx, x, w.enc_stem.conv1_b); + x = ggml_gelu_erf(ctx, x); + x = ggml_cont(ctx, ggml_transpose(ctx, x)); // [d_model, T_enc] + + // + learned positional embedding (prefix view when T_enc < max). + ggml_tensor * pos_emb = w.enc_top.pos_emb_w; + if (T_enc != hp.enc_max_source_positions) { + pos_emb = ggml_view_2d(ctx, w.enc_top.pos_emb_w, d_model, T_enc, w.enc_top.pos_emb_w->nb[1], 0); + } + x = ggml_add(ctx, x, pos_emb); + named(x, "enc.pos_add.out"); + eb.dumps.pos_add_out = x; + transcribe::debug::mark_tensor_for_dump(x); + + const int n_blocks = static_cast(w.enc_blocks.size()); + for (int i = 0; i < n_blocks; ++i) { + x = build_block(ctx, x, w.enc_blocks[i], n_heads, d_model, use_flash); + if (i == 0 || i == n_blocks - 1) { + char nm[64]; + std::snprintf(nm, sizeof(nm), "enc.block.%d.out", i); + named(x, nm); + transcribe::debug::mark_tensor_for_dump(x); + if (i == 0) { + eb.dumps.block_0_out = x; + } else { + eb.dumps.block_last_out = x; + } + } + } + + x = layer_norm(ctx, x, w.enc_top.final_norm_w, w.enc_top.final_norm_b); + named(x, "enc.ln_post.out"); + eb.dumps.ln_post_out = x; + transcribe::debug::mark_tensor_for_dump(x); + + eb.out = x; + ggml_set_output(eb.out); + + eb.graph = ggml_new_graph_custom(ctx, 8192, false); + if (eb.graph == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encoder: ggml_new_graph_custom failed"); + return eb; + } + ggml_build_forward_expand(eb.graph, eb.out); + if (eb.dumps.pos_add_out) { + ggml_build_forward_expand(eb.graph, eb.dumps.pos_add_out); + } + if (eb.dumps.block_0_out) { + ggml_build_forward_expand(eb.graph, eb.dumps.block_0_out); + } + if (eb.dumps.block_last_out) { + ggml_build_forward_expand(eb.graph, eb.dumps.block_last_out); + } + return eb; +} + +AdaptorBuild build_adaptor_graph(ggml_context * ctx, const MossWeights & w, const MossHParams & hp, int T_trim) { + AdaptorBuild ab{}; + const int merge = hp.audio_merge_size; + if (ctx == nullptr || T_trim <= 0 || merge <= 0 || T_trim % merge != 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss adaptor: invalid T_trim=%d (merge=%d)", T_trim, merge); + return ab; + } + const int d_model = hp.enc_d_model; + const int T_enc = T_trim / merge; + ab.T_enc = T_enc; + + ab.in = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, d_model, T_trim); + named(ab.in, "enc.merge.in"); + ggml_set_input(ab.in); + + // 4x time merge: [d_model, T_trim] -> [d_model*merge, T_enc]. Row-major + // group-of-merge over consecutive time frames (contiguous input). cont so + // the tensor is materialized (not a view of the input, whose buffer the + // scheduler recycles after fc1 consumes it — which would corrupt the dump). + ggml_tensor * merged = ggml_cont(ctx, ggml_reshape_2d(ctx, ab.in, static_cast(d_model) * merge, T_enc)); + named(merged, "enc.merge.out"); + ab.dumps.merge_out = merged; + transcribe::debug::mark_tensor_for_dump(merged); + + // VQAdaptor: fc1 + SiLU + fc2 + LayerNorm(eps 1e-6). + ggml_tensor * y = ggml_mul_mat(ctx, w.adaptor.fc1_w, merged); + y = ggml_add(ctx, y, w.adaptor.fc1_b); + y = ggml_silu(ctx, y); + y = ggml_mul_mat(ctx, w.adaptor.fc2_w, y); + y = ggml_add(ctx, y, w.adaptor.fc2_b); + y = layer_norm_eps(ctx, y, w.adaptor.norm_out_w, w.adaptor.norm_out_b, hp.dec_rms_norm_eps); + named(y, "enc.adaptor.out"); + ab.dumps.adaptor_out = y; + transcribe::debug::mark_tensor_for_dump(y); + + ab.out = y; + ggml_set_output(ab.out); + + ab.graph = ggml_new_graph_custom(ctx, 2048, false); + if (ab.graph == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss adaptor: ggml_new_graph_custom failed"); + return ab; + } + ggml_build_forward_expand(ab.graph, ab.out); + if (ab.dumps.merge_out) { + ggml_build_forward_expand(ab.graph, ab.dumps.merge_out); + } + return ab; +} + +} // namespace transcribe::moss diff --git a/src/arch/moss/encoder.h b/src/arch/moss/encoder.h new file mode 100644 index 00000000..229077c8 --- /dev/null +++ b/src/arch/moss/encoder.h @@ -0,0 +1,73 @@ +// arch/moss/encoder.h - MOSS encoder (Whisper) + adaptor graph builders. +// +// Two graphs, run per-chunk then once: +// +// build_encoder_graph: log-mel [n_mels, n_mel_frames] -> Whisper 2-conv stem +// (+GELU) -> + learned positional embedding -> 24 pre-LN blocks -> final +// LayerNorm -> [d_model, T_whisper] (T_whisper = n_mel_frames / 2). One +// chunk per call; the reference pads every 30s chunk to n_samples so +// n_mel_frames is always nb_max_frames (3000) for a full/padded chunk. +// +// build_adaptor_graph: concatenated + per-chunk-trimmed encoder output +// [d_model, T_trim] (T_trim divisible by audio_merge_size) -> 4x time merge +// (reshape to [d_model*merge, T_trim/merge]) -> VQAdaptor (fc1+SiLU+fc2+LN) +// -> [dec_hidden, T_enc] (T_enc = T_trim / merge). +// +// ggml ne (fast innermost): [n_mels, T] for mel, [d_model, T] after the stem. + +#pragma once + +#include "ggml.h" +#include "weights.h" + +struct ggml_context; +struct ggml_cgraph; +struct ggml_tensor; + +namespace transcribe::moss { + +struct EncoderDumps { + ggml_tensor * pos_add_out = nullptr; // post learned-PE add + ggml_tensor * block_0_out = nullptr; + ggml_tensor * block_last_out = nullptr; + ggml_tensor * ln_post_out = nullptr; // after final LayerNorm +}; + +struct EncoderBuild { + ggml_tensor * mel_in = nullptr; // [n_mels, n_mel_frames] + ggml_tensor * out = nullptr; // [d_model, n_mel_frames/2] + EncoderDumps dumps{}; + ggml_cgraph * graph = nullptr; +}; + +EncoderBuild build_encoder_graph(ggml_context * ctx, + const MossWeights & weights, + const MossHParams & hp, + int n_mel_frames, + bool use_flash); + +struct AdaptorDumps { + ggml_tensor * merge_out = nullptr; // [adaptor_input_dim, T_enc] + ggml_tensor * adaptor_out = nullptr; // [dec_hidden, T_enc] +}; + +struct AdaptorBuild { + ggml_tensor * in = nullptr; // [d_model, T_trim] + ggml_tensor * out = nullptr; // [dec_hidden, T_enc] + AdaptorDumps dumps{}; + ggml_cgraph * graph = nullptr; + int T_enc = 0; +}; + +// T_trim must be > 0 and divisible by hp.audio_merge_size. +AdaptorBuild build_adaptor_graph(ggml_context * ctx, const MossWeights & weights, const MossHParams & hp, int T_trim); + +// Whisper conv-stem length reduction: n_mel_frames -> n_mel_frames/2 (conv2 +// stride 2). Exposed for host-side sizing. +int whisper_enc_len(int n_mel_frames); + +// Per-chunk audio token length (processing._compute_audio_token_length): +// (num_samples - 1) / (hop_length * 2 * merge_size) + 1. +int audio_token_length(int num_samples, const MossHParams & hp); + +} // namespace transcribe::moss diff --git a/src/arch/moss/model.cpp b/src/arch/moss/model.cpp new file mode 100644 index 00000000..a366c096 --- /dev/null +++ b/src/arch/moss/model.cpp @@ -0,0 +1,1235 @@ +// arch/moss/model.cpp - MOSS-Transcribe-Diarize family handler. +// +// audio-LLM: Whisper-Medium encoder + 4x time merge + VQAdaptor + Qwen3-0.6B +// causal LM with non-contiguous audio-token injection. Encoder is per-chunk +// (30s Whisper chunks concatenated before the merge); decode reuses the shared +// causal_lm Qwen3 driver (see arch/qwen3_asr for the sibling design). + +#include "causal_lm/causal_lm.h" +#include "decoder.h" +#include "encoder.h" +#include "ggml-backend.h" +#include "ggml.h" +#include "gguf.h" +#include "moss.h" +#include "transcribe-arch.h" +#include "transcribe-batch-util.h" +#include "transcribe-debug.h" +#include "transcribe-env.h" +#include "transcribe-flash-policy.h" +#include "transcribe-load-common.h" +#include "transcribe-loader.h" +#include "transcribe-log.h" +#include "transcribe-mel.h" +#include "transcribe-meta.h" +#include "weights.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace transcribe::moss { + +extern const Arch arch; + +// Per-op profiling under TRANSCRIBE_PERF_DEBUG. The scheduler callback runs +// CPU graphs node-by-node and perturbs fusion; asynchronous GPU timings measure +// dispatch rather than completion. +namespace { + +struct OpProf { + int64_t us[GGML_OP_COUNT] = { 0 }; + int64_t cnt[GGML_OP_COUNT] = { 0 }; + int64_t t0 = 0; // start stamp of the in-flight node +}; + +bool op_prof_eval_cb(ggml_tensor * t, bool ask, void * ud) { + auto * p = static_cast(ud); + if (p == nullptr) { + return true; + } + if (ask) { + p->t0 = ggml_time_us(); + return true; // request the post-op callback + } + const int op = static_cast(t->op); + if (op >= 0 && op < GGML_OP_COUNT) { + p->us[op] += ggml_time_us() - p->t0; + p->cnt[op] += 1; + } + return true; +} + +void op_prof_report(const char * label, const OpProf & p) { + int64_t total_us = 0; + for (int o = 0; o < GGML_OP_COUNT; ++o) { + total_us += p.us[o]; + } + int order[GGML_OP_COUNT]; + for (int o = 0; o < GGML_OP_COUNT; ++o) { + order[o] = o; + } + std::sort(order, order + GGML_OP_COUNT, [&](int a, int b) { return p.us[a] > p.us[b]; }); + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[profile] %s per-op wall (node-by-node) total=%.2f ms:", label, + total_us / 1000.0); + for (int i = 0; i < GGML_OP_COUNT; ++i) { + const int o = order[i]; + if (p.us[o] <= 0) { + break; + } + const double pct = total_us > 0 ? (100.0 * p.us[o] / total_us) : 0.0; + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[profile] %-20s %8.2f ms %5.1f%% x%lld", + ggml_op_name(static_cast(o)), p.us[o] / 1000.0, pct, static_cast(p.cnt[o])); + } +} + +} // namespace + +static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v); + +MossSession::~MossSession() { + kv_cache.free(); + kv_cache_batch.free(); + if (sched != nullptr) { + safe_sched_free(sched); + sched = nullptr; + } + if (compute_ctx != nullptr) { + ggml_free(compute_ctx); + compute_ctx = nullptr; + } +} + +MossModel::~MossModel() { + if (ctx_meta != nullptr) { + ggml_free(ctx_meta); + ctx_meta = nullptr; + } + if (backend_buffer != nullptr) { + safe_buffer_free(backend_buffer); + backend_buffer = nullptr; + } + packed_gate_up.free(); + for (auto it = plan.scheduler_list.rbegin(); it != plan.scheduler_list.rend(); ++it) { + safe_backend_free(*it); + } + plan.scheduler_list.clear(); + plan.primary = nullptr; + plan.primary_kind = transcribe::BackendKind::Unknown; +} + +// --------------------------------------------------------------------------- +// Prompt construction (declared in moss.h) +// --------------------------------------------------------------------------- + +void build_audio_span(const MossHParams & hp, + int audio_seq_len, + std::vector & out_span_ids, + std::vector & out_audio_offsets) { + out_span_ids.clear(); + out_audio_offsets.clear(); + const int32_t audio_id = hp.audio_token_id; + + auto push_audio = [&](int count) { + for (int i = 0; i < count; ++i) { + out_audio_offsets.push_back(static_cast(out_span_ids.size())); + out_span_ids.push_back(audio_id); + } + }; + + if (!hp.enable_time_marker || audio_seq_len <= 0 || hp.time_marker_every_seconds <= 0) { + push_audio(std::max(audio_seq_len, 0)); + return; + } + const int tokens_per_marker = static_cast(hp.audio_tokens_per_second * hp.time_marker_every_seconds); + if (tokens_per_marker <= 0) { + push_audio(audio_seq_len); + return; + } + const int tme = hp.time_marker_every_seconds; + const int duration = static_cast(audio_seq_len / hp.audio_tokens_per_second); + int consumed = 0; + for (int sec = tme; sec <= duration; sec += tme) { + const int pos = (sec / tme) * tokens_per_marker; + const int seg = pos - consumed; + if (seg > 0) { + push_audio(seg); + consumed += seg; + } + // Marker: decimal digits of `sec`, each a single baked digit token. + char buf[16]; + std::snprintf(buf, sizeof(buf), "%d", sec); + for (const char * p = buf; *p != '\0'; ++p) { + out_span_ids.push_back(hp.digit_tokens[static_cast(*p - '0')]); + } + } + const int remainder = audio_seq_len - consumed; + if (remainder > 0) { + push_audio(remainder); + } +} + +void build_prompt_tokens(const MossHParams & hp, + int audio_seq_len, + std::vector & out_ids, + std::vector & out_audio_positions) { + out_ids.clear(); + out_audio_positions.clear(); + + out_ids.insert(out_ids.end(), hp.prompt_prefix_tokens.begin(), hp.prompt_prefix_tokens.end()); + const int prefix_len = static_cast(out_ids.size()); + + std::vector span_ids; + std::vector span_offsets; + build_audio_span(hp, audio_seq_len, span_ids, span_offsets); + out_ids.insert(out_ids.end(), span_ids.begin(), span_ids.end()); + for (int32_t off : span_offsets) { + out_audio_positions.push_back(prefix_len + off); + } + + out_ids.insert(out_ids.end(), hp.prompt_suffix_tokens.begin(), hp.prompt_suffix_tokens.end()); +} + +namespace { + +constexpr const char k_default_variant[] = "moss-transcribe-diarize"; +constexpr int k_max_new = 256; + +int moss_context_ceiling(int32_t n_ctx_knob, const MossHParams & hp) { + int ceiling = hp.dec_max_position_embeddings; + if (n_ctx_knob > 0 && n_ctx_knob < ceiling) { + ceiling = n_ctx_knob; + } + return ceiling; +} + +transcribe_status load(Loader & loader, const transcribe_model_load_params * params, transcribe_model ** out_model) { + const int64_t t_load_start = ggml_time_us(); + + auto m = std::make_unique(); + m->arch = &arch; + m->t_load_us = 0; + m->variant = loader.variant().empty() ? k_default_variant : loader.variant(); + m->backend.clear(); + + apply_family_invariants(*m); + m->caps.n_languages = 0; + m->caps.languages = nullptr; + + if (const transcribe_status st = read_capability_kv(loader.gguf(), m->caps); st != TRANSCRIBE_OK) { + return st; + } + if (const transcribe_status st = read_languages_kv(loader.gguf(), *m); st != TRANSCRIBE_OK) { + return st; + } + + if (const transcribe_status st = m->tok.load(loader.gguf()); st != TRANSCRIBE_OK) { + return st; + } + (void) read_optional_string_kv(loader.gguf(), "tokenizer.chat_template", "moss", "", m->chat_template); + + if (const transcribe_status st = read_moss_hparams(loader.gguf(), m->hparams); st != TRANSCRIBE_OK) { + return st; + } + + m->hparams.vocab_size = m->tok.n_tokens(); + m->hparams.bos_token_id = m->tok.bos_id(); + m->hparams.eos_token_id = m->tok.eos_id(); + if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: tokenizer vocab (%d) != decoder vocab_size (%d)", + m->hparams.vocab_size, m->hparams.dec_vocab_size); + return TRANSCRIBE_ERR_GGUF; + } + if (m->hparams.eos_token_id < 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: GGUF tokenizer has no eos_token_id"); + return TRANSCRIBE_ERR_GGUF; + } + + // Publish an advisory input bound (decoder context / audio-token rate). + if (m->hparams.dec_max_position_embeddings > 0 && m->hparams.fe_hop_length > 0 && m->hparams.fe_sample_rate > 0) { + m->limits.has_context_cap = true; + m->limits.model_max_ctx = m->hparams.dec_max_position_embeddings; + m->limits.prompt_overhead = + static_cast(m->hparams.prompt_prefix_tokens.size() + m->hparams.prompt_suffix_tokens.size()); + m->limits.gen_reserve = k_max_new; + // audio_tokens ~= samples / (hop*2*merge) ; ms = tokens * hop*2*merge*1000/sr + m->limits.ms_per_audio_token = static_cast(m->hparams.fe_hop_length) * 2.0 * + m->hparams.audio_merge_size * 1000.0 / m->hparams.fe_sample_rate; + m->limits.kv_elems_per_ctx_token = + static_cast(m->hparams.dec_n_kv_heads) * m->hparams.dec_head_dim * m->hparams.dec_n_layers * 2; + } + + // Mel frontend (Whisper feature extractor; per-utterance log norm). + { + transcribe::MelConfig cfg{}; + cfg.sample_rate = m->hparams.fe_sample_rate; + cfg.num_mels = m->hparams.fe_num_mels; + cfg.n_fft = m->hparams.fe_n_fft; + cfg.win_length = m->hparams.fe_win_length; + cfg.hop_length = m->hparams.fe_hop_length; + cfg.pre_emphasis = m->hparams.fe_pre_emphasis; + cfg.f_min = m->hparams.fe_f_min; + cfg.f_max = m->hparams.fe_f_max; + cfg.pad_mode = m->hparams.fe_pad_mode; + cfg.window_type = m->hparams.fe_window; + cfg.normalize = m->hparams.fe_normalize; + { + using R = transcribe::load_common::ReadF32Result; + const size_t fb_elems = static_cast(cfg.num_mels) * static_cast(cfg.n_fft / 2 + 1); + const auto fb_rc = transcribe::load_common::read_f32_tensor_checked( + loader.gguf(), loader.path(), "frontend.mel_filterbank", fb_elems, "moss", cfg.filterbank); + if (fb_rc != R::Ok && fb_rc != R::Absent) { + return TRANSCRIBE_ERR_GGUF; + } + const size_t win_elems = static_cast(cfg.win_length); + const auto win_rc = transcribe::load_common::read_f32_tensor_checked( + loader.gguf(), loader.path(), "frontend.window", win_elems, "moss", cfg.window); + if (win_rc != R::Ok && win_rc != R::Absent) { + return TRANSCRIBE_ERR_GGUF; + } + } + m->mel.emplace(cfg); + } + + gguf_init_params init_params{}; + init_params.no_alloc = true; + init_params.ctx = &m->ctx_meta; + gguf_context * gguf_data = gguf_init_from_file(loader.path().c_str(), init_params); + if (gguf_data == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + if (const transcribe_status st = build_moss_weights(m->ctx_meta, m->hparams, m->weights); st != TRANSCRIBE_OK) { + gguf_free(gguf_data); + return st; + } + + const transcribe_backend_request backend_req = (params != nullptr) ? params->backend : TRANSCRIBE_BACKEND_AUTO; + if (const transcribe_status st = transcribe::load_common::init_backends( + backend_req, (params != nullptr) ? params->gpu_device : 0, "moss", m->plan); + st != TRANSCRIBE_OK) { + gguf_free(gguf_data); + return st; + } + m->backend = ggml_backend_name(m->plan.primary); + m->primary_backend = m->plan.primary; + + ggml_backend_buffer_t weights_buffer = ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); + if (weights_buffer == nullptr) { + gguf_free(gguf_data); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: ggml_backend_alloc_ctx_tensors failed"); + return TRANSCRIBE_ERR_GGUF; + } + m->backend_buffer = weights_buffer; + ggml_backend_buffer_set_usage(weights_buffer, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + + if (const transcribe_status st = + transcribe::load_common::stream_tensor_data(loader.path(), gguf_data, m->ctx_meta, "moss"); + st != TRANSCRIBE_OK) { + gguf_free(gguf_data); + return st; + } + gguf_free(gguf_data); + + { + std::vector entries; + entries.reserve(m->weights.dec_blocks.size()); + for (auto & b : m->weights.dec_blocks) { + entries.push_back({ b.ffn_gate_w, b.ffn_up_w, &b.ffn_gate_up_w }); + } + if (!transcribe::causal_lm::pack_gate_up(m->plan.primary, m->hparams.dec_hidden, m->hparams.dec_intermediate, + entries, m->packed_gate_up, "moss")) { + m->packed_gate_up.free(); + return TRANSCRIBE_ERR_GGUF; + } + } + + m->t_load_us = ggml_time_us() - t_load_start; + *out_model = m.release(); + return TRANSCRIBE_OK; +} + +transcribe_status init_context(transcribe_model * model, + const transcribe_session_params * params, + transcribe_session ** out_ctx) { + if (model->arch != &arch) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + auto cc = std::make_unique(); + cc->model = model; + cc->n_threads = params->n_threads; + cc->kv_type = params->kv_type; + cc->n_ctx = transcribe_session_params_n_ctx(params); + + // The Whisper-compatible flash path avoids materializing attention scores. + cc->encoder_use_flash = true; + cc->decoder_use_flash = true; + transcribe::flash::apply_env_overrides(cc->encoder_use_flash, cc->decoder_use_flash); + + auto * cm = static_cast(model); + { + ggml_type kv_type = (cc->kv_type == TRANSCRIBE_KV_TYPE_F32) ? GGML_TYPE_F32 : GGML_TYPE_F16; + if (!transcribe::causal_lm::kv_init(cc->kv_cache, cm->plan.primary, /*n_ctx=*/2048, cm->hparams.dec_n_kv_heads, + cm->hparams.dec_head_dim, cm->hparams.dec_n_layers, kv_type)) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss init_context: KV cache allocation failed"); + return TRANSCRIBE_ERR_OOM; + } + } + *out_ctx = cc.release(); + return TRANSCRIBE_OK; +} + +// --------------------------------------------------------------------------- +// Encoder: chunk audio -> whisper encoder per chunk -> trim -> concat -> +// 4x merge + adaptor -> enc_out [dec_hidden, T_enc]. +// --------------------------------------------------------------------------- + +transcribe_status ensure_sched(MossSession * cc, MossModel * cm) { + if (cc->sched == nullptr) { + cc->sched = ggml_backend_sched_new(cm->plan.scheduler_list.data(), nullptr, + static_cast(cm->plan.scheduler_list.size()), 16384, false, true); + if (cc->sched == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + } + return TRANSCRIBE_OK; +} + +transcribe_status reset_compute_ctx(MossSession * cc, int mb) { + if (cc->compute_ctx != nullptr) { + ggml_free(cc->compute_ctx); + cc->compute_ctx = nullptr; + } + ggml_init_params ip{}; + ip.mem_size = static_cast(mb) * 1024 * 1024; + ip.mem_buffer = nullptr; + ip.no_alloc = true; + cc->compute_ctx = ggml_init(ip); + return cc->compute_ctx != nullptr ? TRANSCRIBE_OK : TRANSCRIBE_ERR_GGUF; +} + +// Fills enc_out [dec_hidden, T_enc]; returns T_enc via out_T_enc. `dumps` marks +// the single-chunk validation case (dumps enc.mel.in + encoder/adaptor stages). +transcribe_status encode_one(MossSession * cc, + MossModel * cm, + const float * pcm, + int n_samples, + bool dumps, + std::vector & enc_out, + int & out_T_enc, + int64_t & mel_us, + int64_t & enc_us) { + const auto & hp = cm->hparams; + const int n_chunk = hp.fe_n_samples > 0 ? hp.fe_n_samples : 30 * hp.fe_sample_rate; + const int d_model = hp.enc_d_model; + const int merge = hp.audio_merge_size; + + if (const transcribe_status st = ensure_sched(cc, cm); st != TRANSCRIBE_OK) { + return st; + } + + std::vector concat_trim; // [d_model, T_trim] column-major (d innermost) + int T_trim_total = 0; + + for (int start = 0; start < n_samples; start += n_chunk) { + const int real_len = std::min(n_chunk, n_samples - start); + const int token_len = audio_token_length(real_len, hp); + if (token_len <= 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: degenerate token_len for chunk (real_len=%d)", real_len); + return TRANSCRIBE_ERR_GGUF; + } + + // Pad chunk to n_chunk samples (zeros) — matches processing._pad_or_trim_audio. + std::vector chunk(static_cast(n_chunk), 0.0f); + std::memcpy(chunk.data(), pcm + start, static_cast(real_len) * sizeof(float)); + + int nm = 0, nf = 0; + const int64_t t_mel0 = ggml_time_us(); + if (const transcribe_status mst = + cm->mel->compute(chunk.data(), static_cast(n_chunk), cc->mel_buf, nm, nf, cc->n_threads); + mst != TRANSCRIBE_OK) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: mel compute failed (%s)", transcribe_status_string(mst)); + return mst; + } + mel_us += ggml_time_us() - t_mel0; + + if (dumps && start == 0 && transcribe::debug::enabled()) { + const long long shape[2] = { nm, nf }; + transcribe::debug::dump_host_f32("enc.mel.in", cc->mel_buf.data(), + static_cast(cc->mel_buf.size()), shape, 2, "frontend.mel.norm"); + } + + // Whisper encoder graph on this chunk's mel [n_mels, nf]. + if (const transcribe_status st = reset_compute_ctx(cc, 16); st != TRANSCRIBE_OK) { + return st; + } + EncoderBuild eb = build_encoder_graph(cc->compute_ctx, cm->weights, hp, nf, cc->encoder_use_flash); + if (eb.graph == nullptr || eb.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, eb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + ggml_backend_tensor_set(eb.mel_in, cc->mel_buf.data(), 0, cc->mel_buf.size() * sizeof(float)); + transcribe::configure_sched_n_threads(cc->sched, cc->n_threads); + + const int64_t t_enc0 = ggml_time_us(); + if (ggml_backend_sched_graph_compute(cc->sched, eb.graph) != GGML_STATUS_SUCCESS) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: encoder graph compute failed"); + return TRANSCRIBE_ERR_GGUF; + } + enc_us += ggml_time_us() - t_enc0; + + if (dumps && start == 0) { + auto try_dump = [](const char * name, ggml_tensor * t, const char * stage) { + if (t != nullptr) { + transcribe::debug::dump_tensor(name, t, stage); + } + }; + try_dump("enc.pos_add.out", eb.dumps.pos_add_out, "enc.pos_add"); + try_dump("enc.block.0.out", eb.dumps.block_0_out, "enc.block.0"); + { + char nm2[64]; + std::snprintf(nm2, sizeof(nm2), "enc.block.%d.out", hp.enc_n_layers - 1); + try_dump(nm2, eb.dumps.block_last_out, "enc.block.last"); + } + try_dump("enc.ln_post.out", eb.dumps.ln_post_out, "enc.ln_post"); + } + + // Read [d_model, T_whisper], trim to first token_len*merge columns. + const int T_whisper = static_cast(eb.out->ne[1]); + const int keep_cols = token_len * merge; + if (keep_cols > T_whisper) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: keep_cols=%d > T_whisper=%d", keep_cols, T_whisper); + return TRANSCRIBE_ERR_GGUF; + } + std::vector chunk_out(static_cast(d_model) * T_whisper); + ggml_backend_tensor_get(eb.out, chunk_out.data(), 0, chunk_out.size() * sizeof(float)); + // Columns are contiguous (d innermost); append the first keep_cols columns. + const size_t old = concat_trim.size(); + concat_trim.resize(old + static_cast(d_model) * keep_cols); + std::memcpy(concat_trim.data() + old, chunk_out.data(), + static_cast(d_model) * keep_cols * sizeof(float)); + T_trim_total += keep_cols; + } + + if (T_trim_total <= 0 || T_trim_total % merge != 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: bad T_trim_total=%d", T_trim_total); + return TRANSCRIBE_ERR_GGUF; + } + + // Adaptor graph on the concatenated, trimmed encoder output. + if (const transcribe_status st = reset_compute_ctx(cc, 16); st != TRANSCRIBE_OK) { + return st; + } + AdaptorBuild ab = build_adaptor_graph(cc->compute_ctx, cm->weights, hp, T_trim_total); + if (ab.graph == nullptr || ab.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, ab.graph)) { + return TRANSCRIBE_ERR_OOM; + } + ggml_backend_tensor_set(ab.in, concat_trim.data(), 0, concat_trim.size() * sizeof(float)); + transcribe::configure_sched_n_threads(cc->sched, cc->n_threads); + const int64_t t_enc1 = ggml_time_us(); + if (ggml_backend_sched_graph_compute(cc->sched, ab.graph) != GGML_STATUS_SUCCESS) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss encode: adaptor graph compute failed"); + return TRANSCRIBE_ERR_GGUF; + } + enc_us += ggml_time_us() - t_enc1; + + if (dumps) { + auto try_dump = [](const char * name, ggml_tensor * t, const char * stage) { + if (t != nullptr) { + transcribe::debug::dump_tensor(name, t, stage); + } + }; + try_dump("enc.merge.out", ab.dumps.merge_out, "enc.merge"); + try_dump("enc.adaptor.out", ab.dumps.adaptor_out, "enc.adaptor"); + } + + out_T_enc = ab.T_enc; + enc_out.resize(static_cast(hp.dec_hidden) * out_T_enc); + ggml_backend_tensor_get(ab.out, enc_out.data(), 0, enc_out.size() * sizeof(float)); + return TRANSCRIBE_OK; +} + +// Build audio_dense [hidden, T_prompt] + keep_mask [1, T_prompt] host-side by +// scattering enc_out columns into the audio-pad prompt positions. +void build_injection(int hidden, + int T_prompt, + const std::vector & enc_out, + const std::vector & audio_positions, + std::vector & audio_dense, + std::vector & keep_mask) { + audio_dense.assign(static_cast(hidden) * T_prompt, 0.0f); + keep_mask.assign(static_cast(T_prompt), 1.0f); + for (size_t j = 0; j < audio_positions.size(); ++j) { + const int pos = audio_positions[j]; + std::memcpy(audio_dense.data() + static_cast(pos) * hidden, enc_out.data() + j * hidden, + static_cast(hidden) * sizeof(float)); + keep_mask[static_cast(pos)] = 0.0f; + } +} + +int32_t argmax_vec(const std::vector & v) { + int32_t best = 0; + float best_v = v.empty() ? 0.0f : v[0]; + for (int32_t i = 1; i < static_cast(v.size()); ++i) { + if (v[i] > best_v) { + best_v = v[i]; + best = i; + } + } + return best; +} + +transcribe_status run(transcribe_session * session, + const float * pcm, + int n_samples, + const transcribe_run_params * params) { + (void) params; + if (session == nullptr || pcm == nullptr || n_samples <= 0) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + auto * cc = static_cast(session); + auto * cm = static_cast(cc->model); + if (cm == nullptr || cm->plan.scheduler_list.empty() || !cm->mel.has_value()) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + if (cc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + transcribe::debug::init(); + const bool dumps_on = transcribe::debug::enabled(); + + // The callback persists across scheduler resets and is reassigned before decode. + const bool perf_debug = transcribe::env::flag("TRANSCRIBE_PERF_DEBUG"); + OpProf enc_prof, dec_prof; + if (perf_debug) { + // Create the lazy scheduler before attaching the callback. + if (const transcribe_status st = ensure_sched(cc, cm); st != TRANSCRIBE_OK) { + return st; + } + ggml_backend_sched_set_eval_callback(cc->sched, op_prof_eval_cb, &enc_prof); + } + + // Encoder. + int T_enc = 0; + if (const transcribe_status st = + encode_one(cc, cm, pcm, n_samples, /*dumps=*/true, cc->enc_host, T_enc, cc->t_mel_us, cc->t_encode_us); + st != TRANSCRIBE_OK) { + return st; + } + + const int64_t t_dec_start = ggml_time_us(); + if (perf_debug) { + ggml_backend_sched_set_eval_callback(cc->sched, op_prof_eval_cb, &dec_prof); + } + + // Prompt. + std::vector prompt_ids; + std::vector audio_positions; + build_prompt_tokens(cm->hparams, T_enc, prompt_ids, audio_positions); + const int T_prompt = static_cast(prompt_ids.size()); + if (static_cast(audio_positions.size()) != T_enc) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss run: audio_positions(%zu) != T_enc(%d)", audio_positions.size(), + T_enc); + return TRANSCRIBE_ERR_GGUF; + } + + const int ceiling = moss_context_ceiling(cc->n_ctx, cm->hparams); + if (T_prompt + k_max_new > ceiling) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moss run: input too long — %d prompt + %d generation exceed the %d-token context.", T_prompt, + k_max_new, ceiling); + return TRANSCRIBE_ERR_INPUT_TOO_LONG; + } + + // Generation budget scales with audio length: the emergent transcript + // (text + [start]/[Sxx]/[end] markers) tracks the audio-token count, which + // for long-form far exceeds the k_max_new floor. Clamp to the context. + const int gen_budget = std::min(ceiling - T_prompt, std::max(k_max_new, 2 * T_enc + 128)); + + // KV cache (grow-to-fit, pow2, clamped to ceiling). + int want_n_ctx = 1024; + while (want_n_ctx < T_prompt + gen_budget) { + want_n_ctx *= 2; + } + if (want_n_ctx > ceiling) { + want_n_ctx = ceiling; + } + if (cc->kv_cache.ctx != nullptr && cc->kv_cache.n_ctx < want_n_ctx) { + cc->kv_cache.free(); + } + if (cc->kv_cache.ctx == nullptr) { + ggml_type kv_type = (cc->kv_type == TRANSCRIBE_KV_TYPE_F32) ? GGML_TYPE_F32 : GGML_TYPE_F16; + if (!transcribe::causal_lm::kv_init(cc->kv_cache, cm->plan.primary, want_n_ctx, cm->hparams.dec_n_kv_heads, + cm->hparams.dec_head_dim, cm->hparams.dec_n_layers, kv_type)) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss run: KV cache allocation failed (n_ctx=%d)", want_n_ctx); + return TRANSCRIBE_ERR_OOM; + } + } else { + if (cc->kv_cache.buffer != nullptr) { + ggml_backend_buffer_clear(cc->kv_cache.buffer, 0); + } + cc->kv_cache.n = 0; + cc->kv_cache.head = 0; + } + + // Prefill. + if (const transcribe_status st = reset_compute_ctx(cc, 16); st != TRANSCRIBE_OK) { + return st; + } + const bool slice_last = !dumps_on; + PrefillBuild pb = build_prefill_graph(cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, T_prompt, + cc->decoder_use_flash, slice_last); + if (pb.graph == nullptr || pb.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, pb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + + ggml_backend_tensor_set(pb.input_ids_in, prompt_ids.data(), 0, prompt_ids.size() * sizeof(int32_t)); + { + std::vector audio_dense, keep_mask; + build_injection(cm->hparams.dec_hidden, T_prompt, cc->enc_host, audio_positions, audio_dense, keep_mask); + ggml_backend_tensor_set(pb.audio_dense_in, audio_dense.data(), 0, audio_dense.size() * sizeof(float)); + ggml_backend_tensor_set(pb.keep_mask_in, keep_mask.data(), 0, keep_mask.size() * sizeof(float)); + } + { + std::vector positions(T_prompt); + for (int i = 0; i < T_prompt; ++i) { + positions[i] = i; + } + ggml_backend_tensor_set(pb.positions_in, positions.data(), 0, positions.size() * sizeof(int32_t)); + } + { + const ggml_fp16_t mz = ggml_fp32_to_fp16(0.0f); + const ggml_fp16_t mn = ggml_fp32_to_fp16(-INFINITY); + std::vector mask(static_cast(T_prompt) * T_prompt, mn); + for (int r = 0; r < T_prompt; ++r) { + for (int c = 0; c <= r; ++c) { + mask[static_cast(r) * T_prompt + c] = mz; + } + } + ggml_backend_tensor_set(pb.mask_in, mask.data(), 0, mask.size() * sizeof(ggml_fp16_t)); + } + + const int64_t t_pf0 = perf_debug ? ggml_time_us() : 0; + if (ggml_backend_sched_graph_compute(cc->sched, pb.graph) != GGML_STATUS_SUCCESS) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss run: prefill compute failed"); + return TRANSCRIBE_ERR_GGUF; + } + const int64_t t_prefill_us = perf_debug ? (ggml_time_us() - t_pf0) : 0; + cc->kv_cache.n = T_prompt; + cc->kv_cache.head = T_prompt; + + if (dumps_on) { + auto try_dump = [](const char * name, ggml_tensor * t, const char * stage) { + if (t != nullptr) { + transcribe::debug::dump_tensor(name, t, stage); + } + }; + try_dump("dec.token_emb", pb.dumps.token_emb, "dec.token_emb"); + try_dump("dec.audio_injected", pb.dumps.audio_injected, "dec.audio_injected"); + try_dump("dec.block.0.out", pb.dumps.block_0_out, "dec.block.0"); + { + char nm[64]; + std::snprintf(nm, sizeof(nm), "dec.block.%d.out", cm->hparams.dec_n_layers - 1); + try_dump(nm, pb.dumps.block_last_out, "dec.block.last"); + } + try_dump("dec.out_before_head", pb.dumps.out_before_head, "dec.out_before_head"); + try_dump("dec.logits_raw", pb.dumps.logits_raw, "dec.logits_raw"); + } + + const int vocab = cm->hparams.dec_vocab_size; + std::vector logits(vocab); + ggml_backend_tensor_get(pb.out, logits.data(), 0, logits.size() * sizeof(float)); + + std::vector generated_ids; + int32_t next_tok = argmax_vec(logits); + generated_ids.push_back(next_tok); + + // Step loop. + const int32_t eos_id = cm->hparams.eos_token_id; + int cur_past = T_prompt; + int max_n_kv = 1024; + while (max_n_kv < T_prompt + gen_budget) { + max_n_kv *= 2; + } + if (max_n_kv > cc->kv_cache.n_ctx) { + max_n_kv = cc->kv_cache.n_ctx; + } + + if (const transcribe_status st = reset_compute_ctx(cc, 8); st != TRANSCRIBE_OK) { + return st; + } + StepBuild sb = + build_step_graph(cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, max_n_kv, cc->decoder_use_flash); + if (sb.graph == nullptr || sb.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, sb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + + const ggml_fp16_t mz = ggml_fp32_to_fp16(0.0f); + const ggml_fp16_t mn = ggml_fp32_to_fp16(-INFINITY); + std::vector step_mask(max_n_kv, mn); + + int gen_dump_step = -1; + if (dumps_on) { + gen_dump_step = 8; // dec.logits_raw.gen8 (mid-generation, n_past>0 coverage) + } + + int64_t t_step_input_us = 0; // host->device tensor_set per step + int64_t t_step_compute_us = 0; // graph compute per step + int64_t t_step_get_us = 0; // device->host argmax read per step + int n_steps = 0; + std::vector per_step_us; + if (perf_debug) { + per_step_us.reserve(512); + } + + while (next_tok != eos_id && static_cast(generated_ids.size()) < gen_budget && cur_past + 1 <= max_n_kv) { + const int64_t t_i0 = perf_debug ? ggml_time_us() : 0; + ggml_backend_tensor_set(sb.input_id_in, &next_tok, 0, sizeof(int32_t)); + const int32_t pos_val = cur_past; + ggml_backend_tensor_set(sb.position_in, &pos_val, 0, sizeof(int32_t)); + const int64_t kv_idx_val = cur_past; + ggml_backend_tensor_set(sb.kv_idx_in, &kv_idx_val, 0, sizeof(int64_t)); + if (cur_past == T_prompt) { + std::fill(step_mask.begin(), step_mask.begin() + cur_past + 1, mz); + } else { + step_mask[cur_past] = mz; + } + ggml_backend_tensor_set(sb.mask_in, step_mask.data(), 0, static_cast(max_n_kv) * sizeof(ggml_fp16_t)); + const int64_t t_c0 = perf_debug ? ggml_time_us() : 0; + if (perf_debug) { + t_step_input_us += t_c0 - t_i0; + } + + if (ggml_backend_sched_graph_compute(cc->sched, sb.graph) != GGML_STATUS_SUCCESS) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss step: graph compute failed"); + return TRANSCRIBE_ERR_GGUF; + } + if (perf_debug) { + const int64_t dt = ggml_time_us() - t_c0; + t_step_compute_us += dt; + per_step_us.push_back(dt); + ++n_steps; + } + + // Mid-generation logits dump (n_past>0 coverage) at a fixed step. + const int steps_done = static_cast(generated_ids.size()); // == n produced before this step's token + if (dumps_on && steps_done == gen_dump_step && sb.logits != nullptr) { + char nm[64]; + std::snprintf(nm, sizeof(nm), "dec.logits_raw.gen%d", gen_dump_step); + transcribe::debug::dump_tensor(nm, sb.logits, "dec.logits_raw.gen"); + } + + const int64_t t_g0 = perf_debug ? ggml_time_us() : 0; + int32_t argmax_tok = 0; + ggml_backend_tensor_get(sb.out, &argmax_tok, 0, sizeof(int32_t)); + if (perf_debug) { + t_step_get_us += ggml_time_us() - t_g0; + } + next_tok = argmax_tok; + generated_ids.push_back(next_tok); + cur_past += 1; + cc->kv_cache.n = cur_past + 1; + cc->kv_cache.head = cur_past + 1; + if (cc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + } + + if (next_tok != eos_id) { + cc->was_truncated = true; + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "moss run: output truncated at %d tokens", + static_cast(generated_ids.size())); + } + if (!generated_ids.empty() && generated_ids.back() == eos_id) { + generated_ids.pop_back(); + } + + // Raw transcript: [start][Sxx]text[end] emergent text. De-diarization is a + // scoring-time concern (WER harness), not the runtime's. + std::string raw_text = cm->tok.decode(generated_ids.data(), static_cast(generated_ids.size())); + + cc->t_decode_us = ggml_time_us() - t_dec_start; + + if (perf_debug) { + ggml_backend_sched_set_eval_callback(cc->sched, nullptr, nullptr); + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile] === MOSS run: mel=%.1f ms encode(graph)=%.1f ms decode=%.1f ms " + "(decoder_use_flash=%d encoder_use_flash=%d)", + cc->t_mel_us / 1000.0, cc->t_encode_us / 1000.0, cc->t_decode_us / 1000.0, + cc->decoder_use_flash ? 1 : 0, cc->encoder_use_flash ? 1 : 0); + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[profile] decode: prefill=%.1f ms steps=%d T_prompt=%d T_enc=%d", + t_prefill_us / 1000.0, n_steps, T_prompt, T_enc); + if (n_steps > 0) { + std::vector sorted = per_step_us; + std::sort(sorted.begin(), sorted.end()); + const auto pct = [&](double p) { + size_t idx = static_cast(p * (n_steps - 1)); + return sorted[std::min(idx, sorted.size() - 1)] / 1000.0; + }; + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile] step compute total=%.1f ms per-step mean=%.3f p50=%.3f p90=%.3f p99=%.3f max=%.3f ms", + t_step_compute_us / 1000.0, (t_step_compute_us / 1000.0) / n_steps, pct(0.50), pct(0.90), pct(0.99), + sorted.back() / 1000.0); + log_msg( + TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile] step overhead: input_set total=%.1f ms (%.3f/step) argmax_get total=%.1f ms (%.3f/step)", + t_step_input_us / 1000.0, (t_step_input_us / 1000.0) / n_steps, t_step_get_us / 1000.0, + (t_step_get_us / 1000.0) / n_steps); + } + op_prof_report("ENCODE", enc_prof); + op_prof_report("DECODE", dec_prof); + } + + cc->full_text = raw_text; + cc->result_kind = TRANSCRIBE_TIMESTAMPS_NONE; + cc->has_result = true; + transcribe_session::SegmentEntry seg{}; + seg.text = raw_text; + seg.t0_ms = 0; + seg.t1_ms = static_cast(n_samples) * 1000 / static_cast(cm->hparams.fe_sample_rate); + cc->segments.push_back(std::move(seg)); + + return cc->was_truncated ? TRANSCRIBE_ERR_OUTPUT_TRUNCATED : TRANSCRIBE_OK; +} + +// --------------------------------------------------------------------------- +// Offline batched decode: per-utterance encoder + prefill (serial into KV +// slabs) or a batched prefill, then a batched step loop. +// --------------------------------------------------------------------------- + +transcribe_session::ResultSet finalize_utterance(MossModel * cm, std::vector generated_ids, int n_samples) { + transcribe_session::ResultSet rs; + const int32_t eos_id = cm->hparams.eos_token_id; + if (!generated_ids.empty() && generated_ids.back() == eos_id) { + generated_ids.pop_back(); + } + std::string raw_text = cm->tok.decode(generated_ids.data(), static_cast(generated_ids.size())); + rs.full_text = raw_text; + rs.result_kind = TRANSCRIBE_TIMESTAMPS_NONE; + rs.has_result = true; + rs.status = TRANSCRIBE_OK; + transcribe_session::SegmentEntry seg{}; + seg.text = raw_text; + seg.t0_ms = 0; + seg.t1_ms = static_cast(n_samples) * 1000 / static_cast(cm->hparams.fe_sample_rate); + rs.segments.push_back(std::move(seg)); + return rs; +} + +transcribe_status run_batch_serial(MossSession * cc, + const float * const * pcm, + const int * n_samples, + int n, + const transcribe_run_params * params) { + bool any_truncated = false; + for (int i = 0; i < n; ++i) { + if (cc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + cc->clear_result(); + cc->t_mel_us = 0; + cc->t_encode_us = 0; + cc->t_decode_us = 0; + cc->was_truncated = false; + + const transcribe_status st = (pcm[i] == nullptr || n_samples[i] <= 0) ? TRANSCRIBE_ERR_INVALID_ARG : + run(cc, pcm[i], n_samples[i], params); + any_truncated = any_truncated || st == TRANSCRIBE_ERR_OUTPUT_TRUNCATED; + if (st == TRANSCRIBE_OK || cc->has_result) { + cc->batch_results.push_back(cc->capture_result(st)); + } else { + transcribe_session::ResultSet rs; + rs.status = st; + cc->batch_results.push_back(std::move(rs)); + } + } + cc->was_truncated = any_truncated; + return TRANSCRIBE_OK; +} + +transcribe_status run_batch(transcribe_session * session, + const float * const * pcm, + const int * n_samples, + int n, + const transcribe_run_params * params) { + if (session == nullptr || pcm == nullptr || n_samples == nullptr || n <= 0) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + auto * cc = static_cast(session); + auto * cm = static_cast(cc->model); + if (cm == nullptr || cm->plan.scheduler_list.empty() || !cm->mel.has_value()) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + // Batched decode requires the flash step path and dump-free operation. + if (!cc->decoder_use_flash || transcribe::debug::enabled() || n == 1) { + return run_batch_serial(cc, pcm, n_samples, n, params); + } + (void) params; + transcribe::debug::init(); + + const int hidden = cm->hparams.dec_hidden; + + // Pass 1: per-utterance encoder + prompt build. + std::vector> enc_hosts(n); + std::vector T_enc(n, 0); + std::vector> prompt_ids(n); + std::vector> audio_positions(n); + std::vector T_prompt(n, 0); + std::vector valid(n, 0); + std::vector fail_status(n, TRANSCRIBE_ERR_INVALID_ARG); + int64_t mel_us = 0, enc_us = 0; + + const int ceiling = moss_context_ceiling(cc->n_ctx, cm->hparams); + int max_T_prompt = 0; + int max_T_enc = 0; + for (int b = 0; b < n; ++b) { + if (cc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + if (pcm[b] == nullptr || n_samples[b] <= 0) { + continue; + } + int te = 0; + const transcribe_status enc_st = + encode_one(cc, cm, pcm[b], n_samples[b], /*dumps=*/false, enc_hosts[b], te, mel_us, enc_us); + if (enc_st != TRANSCRIBE_OK) { + fail_status[b] = enc_st; + continue; + } + T_enc[b] = te; + build_prompt_tokens(cm->hparams, te, prompt_ids[b], audio_positions[b]); + T_prompt[b] = static_cast(prompt_ids[b].size()); + if (T_prompt[b] + k_max_new > ceiling) { + fail_status[b] = TRANSCRIBE_ERR_INPUT_TOO_LONG; + continue; + } + valid[b] = 1; + max_T_prompt = std::max(max_T_prompt, T_prompt[b]); + max_T_enc = std::max(max_T_enc, T_enc[b]); + } + if (max_T_prompt == 0) { + for (int b = 0; b < n; ++b) { + transcribe_session::ResultSet rs; + rs.status = fail_status[b]; + cc->batch_results.push_back(std::move(rs)); + } + return TRANSCRIBE_OK; + } + + // Batch-wide generation budget: covers the longest utterance's transcript + // (scales with its audio tokens), clamped to the context. + const int batch_budget = std::min(ceiling - max_T_prompt, std::max(k_max_new, 2 * max_T_enc + 128)); + + int max_n_kv = 1024; + while (max_n_kv < max_T_prompt + batch_budget) { + max_n_kv *= 2; + } + if (max_n_kv > ceiling) { + max_n_kv = ceiling; + } + + ggml_type kv_type = (cc->kv_type == TRANSCRIBE_KV_TYPE_F32) ? GGML_TYPE_F32 : GGML_TYPE_F16; + if (cc->kv_cache_batch.self_k == nullptr || cc->kv_batch_cap != n || cc->kv_batch_n_ctx != max_n_kv) { + cc->kv_cache_batch.free(); + if (!transcribe::causal_lm::kv_init_batched(cc->kv_cache_batch, cm->plan.primary, max_n_kv, + cm->hparams.dec_n_kv_heads, cm->hparams.dec_head_dim, + cm->hparams.dec_n_layers, n, kv_type)) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss run_batch: batched KV allocation failed"); + return TRANSCRIBE_ERR_OOM; + } + cc->kv_batch_cap = n; + cc->kv_batch_n_ctx = max_n_kv; + } else if (cc->kv_cache_batch.buffer != nullptr) { + ggml_backend_buffer_clear(cc->kv_cache_batch.buffer, 0); + } + + // Pass 2: batched prefill. + std::vector> generated(n); + std::vector next_tok(n, 0); + std::vector n_past(n, 0); + { + int T_prompt_max = max_T_prompt; + if (const transcribe_status st = reset_compute_ctx(cc, 32); st != TRANSCRIBE_OK) { + return st; + } + PrefillBuildBatched pb = build_prefill_graph_batched( + cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache_batch, T_prompt_max, n, cc->decoder_use_flash); + if (pb.graph == nullptr || pb.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + if (const transcribe_status st = ensure_sched(cc, cm); st != TRANSCRIBE_OK) { + return st; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, pb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + + // input_ids [T_prompt_max, n]. + { + std::vector ids(static_cast(T_prompt_max) * n, 0); + for (int b = 0; b < n; ++b) { + if (valid[b]) { + std::memcpy(ids.data() + static_cast(b) * T_prompt_max, prompt_ids[b].data(), + static_cast(T_prompt[b]) * sizeof(int32_t)); + } + } + ggml_backend_tensor_set(pb.input_ids_in, ids.data(), 0, ids.size() * sizeof(int32_t)); + } + // audio_dense + keep (per-utterance scatter at audio_positions). + { + std::vector audio_dense(static_cast(hidden) * T_prompt_max * n, 0.0f); + std::vector keep(static_cast(T_prompt_max) * n, 1.0f); + for (int b = 0; b < n; ++b) { + if (!valid[b]) { + continue; + } + for (size_t j = 0; j < audio_positions[b].size(); ++j) { + const size_t dst_col = static_cast(b) * T_prompt_max + audio_positions[b][j]; + std::memcpy(audio_dense.data() + dst_col * hidden, enc_hosts[b].data() + j * hidden, + static_cast(hidden) * sizeof(float)); + keep[dst_col] = 0.0f; + } + } + ggml_backend_tensor_set(pb.audio_dense_in, audio_dense.data(), 0, audio_dense.size() * sizeof(float)); + ggml_backend_tensor_set(pb.keep_mask_in, keep.data(), 0, keep.size() * sizeof(float)); + } + { + std::vector pos(T_prompt_max); + for (int t = 0; t < T_prompt_max; ++t) { + pos[t] = t; + } + ggml_backend_tensor_set(pb.positions_in, pos.data(), 0, pos.size() * sizeof(int32_t)); + } + { + const ggml_fp16_t mz = ggml_fp32_to_fp16(0.0f); + const ggml_fp16_t mn = ggml_fp32_to_fp16(-INFINITY); + std::vector mask(static_cast(T_prompt_max) * T_prompt_max, mn); + for (int q = 0; q < T_prompt_max; ++q) { + std::fill(mask.begin() + static_cast(q) * T_prompt_max, + mask.begin() + static_cast(q) * T_prompt_max + q + 1, mz); + } + ggml_backend_tensor_set(pb.mask_in, mask.data(), 0, mask.size() * sizeof(ggml_fp16_t)); + } + { + std::vector kidx(static_cast(T_prompt_max) * n); + for (int b = 0; b < n; ++b) { + for (int t = 0; t < T_prompt_max; ++t) { + kidx[static_cast(b) * T_prompt_max + t] = t; + } + } + ggml_backend_tensor_set(pb.kv_idx_in, kidx.data(), 0, kidx.size() * sizeof(int64_t)); + } + { + std::vector lidx(n, 0); + for (int b = 0; b < n; ++b) { + lidx[b] = valid[b] ? (T_prompt[b] - 1) : 0; + } + ggml_backend_tensor_set(pb.last_idx_in, lidx.data(), 0, lidx.size() * sizeof(int32_t)); + } + + transcribe::configure_sched_n_threads(cc->sched, cc->n_threads); + if (ggml_backend_sched_graph_compute(cc->sched, pb.graph) != GGML_STATUS_SUCCESS) { + return TRANSCRIBE_ERR_GGUF; + } + std::vector amax(n, 0); + ggml_backend_tensor_get(pb.out, amax.data(), 0, amax.size() * sizeof(int32_t)); + for (int b = 0; b < n; ++b) { + if (valid[b]) { + n_past[b] = T_prompt[b]; + next_tok[b] = amax[b]; + generated[b].push_back(amax[b]); + } + } + } + + // Pass 3: batched step loop (shared driver). + const int32_t eos_id = cm->hparams.eos_token_id; + if (const transcribe_status st = reset_compute_ctx(cc, 16); st != TRANSCRIBE_OK) { + return st; + } + StepBuildBatched sb = build_step_graph_batched(cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache_batch, + max_n_kv, n, cc->decoder_use_flash); + if (sb.graph == nullptr || sb.out == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + ggml_backend_sched_reset(cc->sched); + if (!ggml_backend_sched_alloc_graph(cc->sched, sb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + + transcribe::causal_lm::StepBatchedIO io{}; + io.input_ids = sb.input_ids_in; + io.positions = sb.position_in; + io.kv_idx = sb.kv_idx_in; + io.mask = sb.mask_in; + io.argmax = sb.out; + io.graph = sb.graph; + + transcribe::causal_lm::StepBatchedState step_state; + step_state.valid = valid; + step_state.next_tok = next_tok; + step_state.n_past = n_past; + + std::vector truncated; + if (const transcribe_status st = transcribe::causal_lm::run_batched_step_loop( + cc, cc->sched, io, n, max_n_kv, eos_id, batch_budget, step_state, generated, nullptr, &truncated); + st != TRANSCRIBE_OK) { + return st; + } + + for (int b = 0; b < n; ++b) { + if (!valid[b]) { + transcribe_session::ResultSet rs; + rs.status = fail_status[b]; + cc->batch_results.push_back(std::move(rs)); + continue; + } + transcribe_session::ResultSet rs = finalize_utterance(cm, generated[b], n_samples[b]); + if (b < static_cast(truncated.size()) && truncated[b]) { + cc->was_truncated = true; + rs.status = TRANSCRIBE_ERR_OUTPUT_TRUNCATED; + } + cc->batch_results.push_back(std::move(rs)); + } + return TRANSCRIBE_OK; +} + +} // namespace + +extern const Arch arch = { + /* .name = */ "moss", + /* .load = */ load, + /* .init_context = */ init_context, + /* .run = */ run, + /* .run_batch = */ run_batch, + /* .stream_validate = */ nullptr, + /* .stream_begin = */ nullptr, + /* .stream_feed = */ nullptr, + /* .stream_finalize = */ nullptr, + /* .stream_reset = */ nullptr, + /* .accepts_ext_kind = */ nullptr, +}; + +} // namespace transcribe::moss diff --git a/src/arch/moss/moss.h b/src/arch/moss/moss.h new file mode 100644 index 00000000..515e4792 --- /dev/null +++ b/src/arch/moss/moss.h @@ -0,0 +1,104 @@ +// arch/moss/moss.h - MOSS-Transcribe-Diarize model and context types. +// +// INTERNAL to src/arch/moss/. Concrete transcribe_model / transcribe_session +// subclasses for the MOSS family (audio-LLM: Whisper-Medium encoder + 4x time +// merge + VQAdaptor + Qwen3-0.6B causal LM with audio-token injection). +// +// Reuses src/causal_lm/ for the Qwen3 decoder block math (identical to +// arch/qwen3_asr) and a whisper-style encoder graph (arch/whisper). MOSS- +// specific pieces: the 4x temporal merge + VQAdaptor bridge, the non-contiguous +// audio-token injection (time-marker digits interleave the audio span), and the +// fixed baked prompt. + +#pragma once + +#include "causal_lm/causal_lm.h" +#include "ggml-backend.h" +#include "ggml.h" +#include "transcribe-backend.h" +#include "transcribe-mel.h" +#include "transcribe-model.h" +#include "transcribe-session.h" +#include "transcribe-tokenizer.h" +#include "weights.h" + +#include +#include +#include +#include + +struct ggml_context; +struct ggml_tensor; +struct ggml_backend; +struct ggml_backend_buffer; +struct ggml_backend_sched; +typedef struct ggml_backend * ggml_backend_t; +typedef struct ggml_backend_buffer * ggml_backend_buffer_t; +typedef struct ggml_backend_sched * ggml_backend_sched_t; + +namespace transcribe::moss { + +void apply_family_invariants(transcribe_model & model); + +// Reproduce processing_moss_transcribe_diarize._audio_span_ids: the audio +// placeholder span with time-marker digit tokens interleaved. Deterministic +// integer math (no BPE); the digit ids come from the baked digit_tokens KV. +// out_audio_positions receives the prompt-relative indices (0-based within the +// span) of the audio_pad tokens, in order — the j-th audio feature lands at +// span position out_audio_positions[j]. +void build_audio_span(const MossHParams & hp, + int audio_seq_len, + std::vector & out_span_ids, + std::vector & out_audio_offsets); + +// Assemble the full prompt: prefix_tokens + audio_span + suffix_tokens. +// out_audio_positions holds the absolute prompt positions of the audio_pad +// tokens (in order), so the b-th audio feature is scattered to +// input_ids[out_audio_positions[b]]. +void build_prompt_tokens(const MossHParams & hp, + int audio_seq_len, + std::vector & out_ids, + std::vector & out_audio_positions); + +struct MossModel final : public transcribe_model { + Tokenizer tok; + MossHParams hparams; + MossWeights weights; + ggml_context * ctx_meta = nullptr; + + transcribe::BackendPlan plan; + ggml_backend_buffer_t backend_buffer = nullptr; + transcribe::causal_lm::PackedGateUpHandles packed_gate_up; + + std::optional mel; + + std::string chat_template; // informational; not used at inference + + MossModel() = default; + ~MossModel() override; + + const transcribe::Tokenizer * tokenizer() const override { return &tok; } +}; + +struct MossSession final : public transcribe_session { + ggml_context * compute_ctx = nullptr; + ggml_backend_sched_t sched = nullptr; + + transcribe::causal_lm::KvCache kv_cache; + + // Batched KV cache for offline transcribe_run_batch (n_batch slabs). + transcribe::causal_lm::KvCache kv_cache_batch; + int kv_batch_cap = 0; + int kv_batch_n_ctx = 0; + + std::vector mel_buf; + std::vector enc_host; // adaptor output [dec_hidden, T_enc], pre-injection + + bool encoder_use_flash = false; + bool decoder_use_flash = true; + + MossSession() = default; + ~MossSession() override; +}; + +} // namespace transcribe::moss diff --git a/src/arch/moss/weights.cpp b/src/arch/moss/weights.cpp new file mode 100644 index 00000000..d02b3ca7 --- /dev/null +++ b/src/arch/moss/weights.cpp @@ -0,0 +1,443 @@ +// arch/moss/weights.cpp - read_moss_hparams + build_moss_weights. +// +// Mirrors arch/qwen3_asr/weights.cpp: every required hparam is read explicitly +// (BadType fatal), and the tensor catalog is a sequence of typed get_tensor() +// calls with expected shapes. The lm_head slot does not exist — MOSS ties the +// output projection to dec.token_embd.weight. + +#include "weights.h" + +#include "ggml.h" +#include "gguf.h" +#include "transcribe-log.h" +#include "transcribe-meta.h" +#include "transcribe-weights-util.h" + +#include + +namespace transcribe::moss { + +namespace { +constexpr const char * kFamilyTag = "moss"; + +// Read a required non-empty INT32 array KV into `out`. +transcribe_status read_required_i32_array(const gguf_context * gguf, const char * key, std::vector & out) { + switch (read_int32_array_kv(gguf, key, out)) { + case KvResult::Ok: + if (out.empty()) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: KV %s is an empty array", key); + return TRANSCRIBE_ERR_GGUF; + } + return TRANSCRIBE_OK; + case KvResult::Absent: + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: missing required KV %s", key); + return TRANSCRIBE_ERR_GGUF; + case KvResult::BadType: + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: KV %s has wrong type (want i32 array)", key); + return TRANSCRIBE_ERR_GGUF; + } + return TRANSCRIBE_ERR_GGUF; // unreachable +} +} // namespace + +transcribe_status read_moss_hparams(const gguf_context * gguf, MossHParams & hp) { + if (gguf == nullptr) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + + // Encoder (Whisper). + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.n_layers", kFamilyTag, hp.enc_n_layers); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.d_model", kFamilyTag, hp.enc_d_model); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.n_heads", kFamilyTag, hp.enc_n_heads); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.ffn_dim", kFamilyTag, hp.enc_ffn_dim); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.num_mel_bins", kFamilyTag, hp.enc_num_mel_bins); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.encoder.max_source_positions", kFamilyTag, + hp.enc_max_source_positions); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_string_kv(gguf, "stt.moss.encoder.activation", kFamilyTag, hp.enc_activation); + st != TRANSCRIBE_OK) { + return st; + } + + // Adaptor. + if (auto st = read_required_u32_kv(gguf, "stt.moss.adaptor.input_dim", kFamilyTag, hp.adaptor_input_dim); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.audio_merge_size", kFamilyTag, hp.audio_merge_size); + st != TRANSCRIBE_OK) { + return st; + } + + // Decoder (Qwen3). + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.n_layers", kFamilyTag, hp.dec_n_layers); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.hidden_size", kFamilyTag, hp.dec_hidden); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.intermediate_size", kFamilyTag, hp.dec_intermediate); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.n_heads", kFamilyTag, hp.dec_n_heads); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.n_kv_heads", kFamilyTag, hp.dec_n_kv_heads); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.head_dim", kFamilyTag, hp.dec_head_dim); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_string_kv(gguf, "stt.moss.decoder.hidden_act", kFamilyTag, hp.dec_hidden_act); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.moss.decoder.rms_norm_eps", kFamilyTag, hp.dec_rms_norm_eps); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.moss.decoder.rope_theta", kFamilyTag, hp.dec_rope_theta); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.max_position_embeddings", kFamilyTag, + hp.dec_max_position_embeddings); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional_bool_kv(gguf, "stt.moss.decoder.tie_word_embeddings", kFamilyTag, true, + hp.dec_tie_word_embeddings); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.moss.decoder.vocab_size", kFamilyTag, hp.dec_vocab_size); + st != TRANSCRIBE_OK) { + return st; + } + + // Audio injection + time-marker span. + if (auto st = read_required_u32_kv(gguf, "stt.moss.audio_token_id", kFamilyTag, hp.audio_token_id); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = + read_required_f32_kv(gguf, "stt.moss.audio_tokens_per_second", kFamilyTag, hp.audio_tokens_per_second); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = + read_required_u32_kv(gguf, "stt.moss.time_marker_every_seconds", kFamilyTag, hp.time_marker_every_seconds); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional_bool_kv(gguf, "stt.moss.enable_time_marker", kFamilyTag, true, hp.enable_time_marker); + st != TRANSCRIBE_OK) { + return st; + } + + // Baked prompt tokens. + if (auto st = read_required_i32_array(gguf, "stt.moss.prompt_prefix_tokens", hp.prompt_prefix_tokens); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_i32_array(gguf, "stt.moss.prompt_suffix_tokens", hp.prompt_suffix_tokens); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_i32_array(gguf, "stt.moss.digit_tokens", hp.digit_tokens); st != TRANSCRIBE_OK) { + return st; + } + if (hp.digit_tokens.size() != 10) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: stt.moss.digit_tokens must have 10 entries, got %zu", + hp.digit_tokens.size()); + return TRANSCRIBE_ERR_GGUF; + } + + // Frontend. + if (auto st = read_required_string_kv(gguf, "stt.frontend.type", kFamilyTag, hp.fe_type); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.frontend.num_mels", kFamilyTag, hp.fe_num_mels); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.frontend.sample_rate", kFamilyTag, hp.fe_sample_rate); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.frontend.n_fft", kFamilyTag, hp.fe_n_fft); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.frontend.win_length", kFamilyTag, hp.fe_win_length); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.frontend.hop_length", kFamilyTag, hp.fe_hop_length); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_string_kv(gguf, "stt.frontend.window", kFamilyTag, hp.fe_window); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_string_kv(gguf, "stt.frontend.normalize", kFamilyTag, hp.fe_normalize); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.frontend.dither", kFamilyTag, hp.fe_dither); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.frontend.pre_emphasis", kFamilyTag, hp.fe_pre_emphasis); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.frontend.f_min", kFamilyTag, hp.fe_f_min); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_f32_kv(gguf, "stt.frontend.f_max", kFamilyTag, hp.fe_f_max); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional_string_kv(gguf, "stt.frontend.pad_mode", kFamilyTag, "reflect", hp.fe_pad_mode); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional_string_kv(gguf, "stt.frontend.mel_norm", kFamilyTag, "slaney", hp.fe_mel_norm); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional_bool_kv(gguf, "stt.frontend.center", kFamilyTag, true, hp.fe_center); + st != TRANSCRIBE_OK) { + return st; + } + { + const auto read_optional = [&](const char * key, int32_t & dst) -> transcribe_status { + uint32_t tmp = 0; + switch (read_uint32_kv(gguf, key, tmp)) { + case KvResult::Ok: + dst = static_cast(tmp); + return TRANSCRIBE_OK; + case KvResult::Absent: + return TRANSCRIBE_OK; + case KvResult::BadType: + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: \"%s\" has wrong type", key); + return TRANSCRIBE_ERR_GGUF; + } + return TRANSCRIBE_ERR_GGUF; + }; + if (auto st = read_optional("stt.frontend.chunk_length", hp.fe_chunk_length); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional("stt.frontend.n_samples", hp.fe_n_samples); st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_optional("stt.frontend.nb_max_frames", hp.fe_nb_max_frames); st != TRANSCRIBE_OK) { + return st; + } + } + + // Cross-field invariants. + if (hp.enc_n_layers <= 0 || hp.enc_d_model <= 0 || hp.enc_n_heads <= 0 || hp.enc_ffn_dim <= 0 || + hp.enc_num_mel_bins <= 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: encoder hparams must be positive"); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.enc_d_model % hp.enc_n_heads != 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, + hp.enc_n_heads); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.enc_activation != "gelu") { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: unsupported encoder activation \"%s\" (only gelu)", + hp.enc_activation.c_str()); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.audio_merge_size <= 0 || hp.adaptor_input_dim != hp.dec_hidden * hp.audio_merge_size) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: adaptor_input_dim (%d) != dec_hidden (%d) * merge (%d)", + hp.adaptor_input_dim, hp.dec_hidden, hp.audio_merge_size); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || + hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: decoder hparams must be positive"); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, + hp.dec_n_kv_heads); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.dec_hidden_act != "silu" && hp.dec_hidden_act != "swish") { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: unsupported decoder hidden_act \"%s\" (only silu/swish)", + hp.dec_hidden_act.c_str()); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.fe_type != "mel") { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: unsupported frontend type \"%s\"", hp.fe_type.c_str()); + return TRANSCRIBE_ERR_GGUF; + } + if (!hp.dec_tie_word_embeddings) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moss: decoder.tie_word_embeddings=false is not supported (graph assumes tied lm_head)"); + return TRANSCRIBE_ERR_GGUF; + } + return TRANSCRIBE_OK; +} + +// --------------------------------------------------------------------------- +// Weights +// --------------------------------------------------------------------------- + +namespace { + +using transcribe::weights::lname; +constexpr const char * kTag = kFamilyTag; + +#define GET_F32(slot, name, ...) \ + do { \ + ggml_tensor * _t = \ + transcribe::weights::find_tensor(ctx_meta, (name), { GGML_TYPE_F32 }, { __VA_ARGS__ }, kTag); \ + if (_t == nullptr) \ + return TRANSCRIBE_ERR_GGUF; \ + (slot) = _t; \ + } while (0) + +#define GET_CONV(slot, name, ...) \ + do { \ + ggml_tensor * _t = transcribe::weights::find_tensor(ctx_meta, (name), { TRANSCRIBE_QUANT_CONV_TYPES }, \ + { __VA_ARGS__ }, kTag); \ + if (_t == nullptr) \ + return TRANSCRIBE_ERR_GGUF; \ + (slot) = _t; \ + } while (0) + +#define GET_LIN(slot, name, ...) \ + do { \ + ggml_tensor * _t = transcribe::weights::find_tensor(ctx_meta, (name), { TRANSCRIBE_QUANT_LINEAR_TYPES }, \ + { __VA_ARGS__ }, kTag); \ + if (_t == nullptr) \ + return TRANSCRIBE_ERR_GGUF; \ + (slot) = _t; \ + } while (0) + +} // namespace + +transcribe_status build_moss_weights(ggml_context * ctx_meta, const MossHParams & hp, MossWeights & weights) { + if (ctx_meta == nullptr) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + + const int64_t d_model = hp.enc_d_model; + const int64_t ffn_dim = hp.enc_ffn_dim; + const int64_t n_mels = hp.enc_num_mel_bins; + const int64_t adp_in = hp.adaptor_input_dim; + + // ----- encoder: conv stem ----- + GET_CONV(weights.enc_stem.conv0_w, "enc.conv.0.weight", 3, n_mels, d_model); + GET_F32(weights.enc_stem.conv0_b, "enc.conv.0.bias", d_model); + GET_CONV(weights.enc_stem.conv1_w, "enc.conv.1.weight", 3, d_model, d_model); + GET_F32(weights.enc_stem.conv1_b, "enc.conv.1.bias", d_model); + + // ----- encoder: top (pos emb + final LN) ----- + GET_F32(weights.enc_top.pos_emb_w, "enc.pos_emb.weight", d_model, hp.enc_max_source_positions); + GET_F32(weights.enc_top.final_norm_w, "enc.final_norm.weight", d_model); + GET_F32(weights.enc_top.final_norm_b, "enc.final_norm.bias", d_model); + + // ----- encoder: blocks (q/v/out bias; k none) ----- + weights.enc_blocks.assign(hp.enc_n_layers, MossEncBlock{}); + for (int i = 0; i < hp.enc_n_layers; ++i) { + auto & b = weights.enc_blocks[i]; + GET_F32(b.norm_attn_w, lname("enc.blocks.%d.norm_attn.weight", i), d_model); + GET_F32(b.norm_attn_b, lname("enc.blocks.%d.norm_attn.bias", i), d_model); + GET_LIN(b.attn_q_w, lname("enc.blocks.%d.attn.q.weight", i), d_model, d_model); + GET_F32(b.attn_q_b, lname("enc.blocks.%d.attn.q.bias", i), d_model); + GET_LIN(b.attn_k_w, lname("enc.blocks.%d.attn.k.weight", i), d_model, d_model); + GET_LIN(b.attn_v_w, lname("enc.blocks.%d.attn.v.weight", i), d_model, d_model); + GET_F32(b.attn_v_b, lname("enc.blocks.%d.attn.v.bias", i), d_model); + GET_LIN(b.attn_out_w, lname("enc.blocks.%d.attn.out.weight", i), d_model, d_model); + GET_F32(b.attn_out_b, lname("enc.blocks.%d.attn.out.bias", i), d_model); + GET_F32(b.norm_ffn_w, lname("enc.blocks.%d.norm_ffn.weight", i), d_model); + GET_F32(b.norm_ffn_b, lname("enc.blocks.%d.norm_ffn.bias", i), d_model); + GET_LIN(b.ffn_fc1_w, lname("enc.blocks.%d.ffn.fc1.weight", i), d_model, ffn_dim); + GET_F32(b.ffn_fc1_b, lname("enc.blocks.%d.ffn.fc1.bias", i), ffn_dim); + GET_LIN(b.ffn_fc2_w, lname("enc.blocks.%d.ffn.fc2.weight", i), ffn_dim, d_model); + GET_F32(b.ffn_fc2_b, lname("enc.blocks.%d.ffn.fc2.bias", i), d_model); + } + + // ----- adaptor ----- + GET_LIN(weights.adaptor.fc1_w, "adaptor.fc1.weight", adp_in, hp.dec_hidden); + GET_F32(weights.adaptor.fc1_b, "adaptor.fc1.bias", hp.dec_hidden); + GET_LIN(weights.adaptor.fc2_w, "adaptor.fc2.weight", hp.dec_hidden, hp.dec_hidden); + GET_F32(weights.adaptor.fc2_b, "adaptor.fc2.bias", hp.dec_hidden); + GET_F32(weights.adaptor.norm_out_w, "adaptor.norm_out.weight", hp.dec_hidden); + GET_F32(weights.adaptor.norm_out_b, "adaptor.norm_out.bias", hp.dec_hidden); + + // ----- decoder: embedding (tied output) ----- + { + ggml_tensor * tw = ggml_get_tensor(ctx_meta, "dec.token_embd.weight"); + if (tw == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: missing tensor \"dec.token_embd.weight\""); + return TRANSCRIBE_ERR_GGUF; + } + if (tw->ne[0] != hp.dec_hidden || tw->ne[1] != hp.dec_vocab_size) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: dec.token_embd.weight shape [%lld,%lld], expected [%lld,%lld]", + static_cast(tw->ne[0]), static_cast(tw->ne[1]), + static_cast(hp.dec_hidden), static_cast(hp.dec_vocab_size)); + return TRANSCRIBE_ERR_GGUF; + } + weights.dec_embed.token_w = tw; + } + + // ----- decoder: blocks ----- + const int64_t dec_h = hp.dec_hidden; + const int64_t dec_hd = hp.dec_head_dim; + const int64_t dec_im = hp.dec_intermediate; + const int64_t q_out = static_cast(hp.dec_n_heads) * dec_hd; + const int64_t kv_out = static_cast(hp.dec_n_kv_heads) * dec_hd; + + weights.dec_blocks.assign(hp.dec_n_layers, MossDecBlock{}); + for (int i = 0; i < hp.dec_n_layers; ++i) { + auto & b = weights.dec_blocks[i]; + GET_F32(b.norm_attn_w, lname("dec.blocks.%d.norm_attn.weight", i), dec_h); + GET_F32(b.norm_ffn_w, lname("dec.blocks.%d.norm_ffn.weight", i), dec_h); + GET_LIN(b.attn_q_w, lname("dec.blocks.%d.attn.q.weight", i), dec_h, q_out); + GET_LIN(b.attn_k_w, lname("dec.blocks.%d.attn.k.weight", i), dec_h, kv_out); + GET_LIN(b.attn_v_w, lname("dec.blocks.%d.attn.v.weight", i), dec_h, kv_out); + GET_LIN(b.attn_o_w, lname("dec.blocks.%d.attn.o.weight", i), q_out, dec_h); + GET_F32(b.attn_q_norm, lname("dec.blocks.%d.attn.q_norm.weight", i), dec_hd); + GET_F32(b.attn_k_norm, lname("dec.blocks.%d.attn.k_norm.weight", i), dec_hd); + GET_LIN(b.ffn_gate_w, lname("dec.blocks.%d.ffn.gate.weight", i), dec_h, dec_im); + GET_LIN(b.ffn_up_w, lname("dec.blocks.%d.ffn.up.weight", i), dec_h, dec_im); + GET_LIN(b.ffn_down_w, lname("dec.blocks.%d.ffn.down.weight", i), dec_im, dec_h); + if (b.ffn_gate_w->type != b.ffn_up_w->type) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moss: ffn gate/up dtype mismatch at layer %d", i); + return TRANSCRIBE_ERR_GGUF; + } + } + + GET_F32(weights.dec_final.norm_w, "dec.output_norm.weight", dec_h); + return TRANSCRIBE_OK; +} + +} // namespace transcribe::moss diff --git a/src/arch/moss/weights.h b/src/arch/moss/weights.h new file mode 100644 index 00000000..fcfd540b --- /dev/null +++ b/src/arch/moss/weights.h @@ -0,0 +1,182 @@ +// arch/moss/weights.h - MOSS-Transcribe-Diarize tensor catalog and hparams. +// +// INTERNAL to src/arch/moss/. Two-sided audio-LLM layout: +// +// enc.* Whisper-Medium encoder (2x Conv1d stem + learned positional +// embedding + 24 pre-LN LayerNorm transformer blocks, gelu FFN; +// q/v/out have bias, k does not). Matches the `whisper` family. +// adaptor.* 4x-time-merge bridge: Linear(4096->1024) + SiLU + +// Linear(1024->1024) + LayerNorm(bias). Merge itself is a reshape. +// dec.* Qwen3-0.6B causal LM (28 layers, GQA 16/8, head_dim 128, per-head +// q/k RMSNorm, SwiGLU, NeoX RoPE theta 1e6, tied lm_head). Identical +// tensor names + block math to the `qwen3_asr` text LM. +// +// Linear weights use ggml [in, out] order; Conv1d kernels are [K, IC, OC]. + +#pragma once + +#include "transcribe.h" + +#include +#include +#include + +struct gguf_context; +struct ggml_context; +struct ggml_tensor; + +namespace transcribe::moss { + +struct MossHParams { + // Whisper encoder. + int32_t enc_n_layers = 0; + int32_t enc_d_model = 0; + int32_t enc_n_heads = 0; + int32_t enc_ffn_dim = 0; + int32_t enc_num_mel_bins = 0; + int32_t enc_max_source_positions = 0; + std::string enc_activation; // "gelu" + + // VQAdaptor. + int32_t adaptor_input_dim = 0; // 4096 (= dec_hidden * audio_merge_size) + int32_t audio_merge_size = 0; // 4 + + // Qwen3 text LM. + int32_t dec_n_layers = 0; + int32_t dec_hidden = 0; + int32_t dec_intermediate = 0; + int32_t dec_n_heads = 0; + int32_t dec_n_kv_heads = 0; + int32_t dec_head_dim = 0; + std::string dec_hidden_act; // "silu" + float dec_rms_norm_eps = 0.0f; + float dec_rope_theta = 0.0f; + int32_t dec_max_position_embeddings = 0; + bool dec_tie_word_embeddings = true; + int32_t dec_vocab_size = 0; + + // Audio-token injection + time-marker span construction (processor). + int32_t audio_token_id = 0; // <|audio_pad|> (151671) + float audio_tokens_per_second = 0.0f; // 12.5 + int32_t time_marker_every_seconds = 0; // 5 + bool enable_time_marker = true; + + // Baked fixed prompt (see convert-moss.py::compute_prompt_tokens). + std::vector prompt_prefix_tokens; + std::vector prompt_suffix_tokens; + std::vector digit_tokens; // ids for '0'..'9' + + // Token ids (resolved from tokenizer KV at load). + int32_t bos_token_id = -1; + int32_t eos_token_id = -1; + int32_t pad_token_id = -1; + int32_t vocab_size = 0; + + // Frontend (Whisper feature extractor). + std::string fe_type; + int32_t fe_num_mels = 0; + int32_t fe_sample_rate = 0; + int32_t fe_n_fft = 0; + int32_t fe_win_length = 0; + int32_t fe_hop_length = 0; + std::string fe_window; + std::string fe_normalize; + float fe_dither = 0.0f; + float fe_pre_emphasis = 0.0f; + float fe_f_min = 0.0f; + float fe_f_max = 0.0f; + std::string fe_pad_mode; + bool fe_center = true; + std::string fe_mel_norm; + int32_t fe_chunk_length = 0; + int32_t fe_n_samples = 0; + int32_t fe_nb_max_frames = 0; +}; + +transcribe_status read_moss_hparams(const gguf_context * gguf, MossHParams & hp); + +// --------------------------------------------------------------------------- +// Weight slots +// --------------------------------------------------------------------------- + +// Whisper 2-layer Conv1d stem. +struct MossEncStem { + ggml_tensor * conv0_w = nullptr; // [3, num_mel_bins, d_model] + ggml_tensor * conv0_b = nullptr; // [d_model] + ggml_tensor * conv1_w = nullptr; // [3, d_model, d_model] + ggml_tensor * conv1_b = nullptr; // [d_model] +}; + +struct MossEncTop { + ggml_tensor * pos_emb_w = nullptr; // [d_model, max_source_positions] + ggml_tensor * final_norm_w = nullptr; // [d_model] + ggml_tensor * final_norm_b = nullptr; // [d_model] +}; + +// One Whisper pre-LN encoder block (q/v/out have bias, k does not). +struct MossEncBlock { + ggml_tensor * norm_attn_w = nullptr; + ggml_tensor * norm_attn_b = nullptr; + ggml_tensor * attn_q_w = nullptr; + ggml_tensor * attn_q_b = nullptr; + ggml_tensor * attn_k_w = nullptr; // no bias + ggml_tensor * attn_v_w = nullptr; + ggml_tensor * attn_v_b = nullptr; + ggml_tensor * attn_out_w = nullptr; + ggml_tensor * attn_out_b = nullptr; + ggml_tensor * norm_ffn_w = nullptr; + ggml_tensor * norm_ffn_b = nullptr; + ggml_tensor * ffn_fc1_w = nullptr; // [d_model, ffn_dim] + ggml_tensor * ffn_fc1_b = nullptr; + ggml_tensor * ffn_fc2_w = nullptr; // [ffn_dim, d_model] + ggml_tensor * ffn_fc2_b = nullptr; +}; + +// VQAdaptor: Linear(4096->1024) + SiLU + Linear(1024->1024) + LayerNorm. +struct MossAdaptor { + ggml_tensor * fc1_w = nullptr; // [adaptor_input_dim, dec_hidden] + ggml_tensor * fc1_b = nullptr; // [dec_hidden] + ggml_tensor * fc2_w = nullptr; // [dec_hidden, dec_hidden] + ggml_tensor * fc2_b = nullptr; + ggml_tensor * norm_out_w = nullptr; // [dec_hidden] + ggml_tensor * norm_out_b = nullptr; +}; + +struct MossDecEmbed { + ggml_tensor * token_w = nullptr; // [hidden, vocab_size] — tied to lm_head +}; + +// Qwen3 block (mirrors qwen3_asr QwenAsrDecBlock for causal_lm::BlockView). +struct MossDecBlock { + ggml_tensor * norm_attn_w = nullptr; + ggml_tensor * norm_ffn_w = nullptr; + ggml_tensor * attn_q_w = nullptr; + ggml_tensor * attn_k_w = nullptr; + ggml_tensor * attn_v_w = nullptr; + ggml_tensor * attn_o_w = nullptr; + ggml_tensor * attn_q_norm = nullptr; // [head_dim] + ggml_tensor * attn_k_norm = nullptr; // [head_dim] + ggml_tensor * ffn_gate_w = nullptr; + ggml_tensor * ffn_up_w = nullptr; + ggml_tensor * ffn_down_w = nullptr; + ggml_tensor * ffn_gate_up_w = nullptr; // packed at load +}; + +struct MossDecFinal { + ggml_tensor * norm_w = nullptr; +}; + +struct MossWeights { + MossEncStem enc_stem; + MossEncTop enc_top; + std::vector enc_blocks; + MossAdaptor adaptor; + + MossDecEmbed dec_embed; + std::vector dec_blocks; + MossDecFinal dec_final; +}; + +transcribe_status build_moss_weights(ggml_context * ctx_meta, const MossHParams & hp, MossWeights & weights); + +} // namespace transcribe::moss diff --git a/src/transcribe-arch.cpp b/src/transcribe-arch.cpp index 27010ad7..c75f478a 100644 --- a/src/transcribe-arch.cpp +++ b/src/transcribe-arch.cpp @@ -27,6 +27,10 @@ namespace qwen3_asr { extern const Arch arch; } +namespace moss { +extern const Arch arch; +} + namespace voxtral { extern const Arch arch; } @@ -84,7 +88,7 @@ const Arch * find_arch(const char * name) { ¶keet::arch, &cohere::arch, &canary::arch, &qwen3_asr::arch, &voxtral::arch, &voxtral_realtime::arch, &canary_qwen::arch, &whisper::arch, &moonshine::arch, &moonshine_streaming::arch, &sensevoice::arch, &funasr_nano::arch, &gigaam::arch, &granite::arch, &granite_nar::arch, - &medasr::arch, + &medasr::arch, &moss::arch, }; constexpr size_t k_n = sizeof(k_archs) / sizeof(k_archs[0]); diff --git a/tests/golden/batch/moss-transcribe-diarize.cpu.json b/tests/golden/batch/moss-transcribe-diarize.cpu.json new file mode 100644 index 00000000..47b397c5 --- /dev/null +++ b/tests/golden/batch/moss-transcribe-diarize.cpu.json @@ -0,0 +1,19 @@ +{ + "model": "models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf", + "backend": "cpu", + "language": "en", + "texts": { + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0000.wav": "[0.48][S01] He hoped there would be stew for dinner, turnips and carrots and bruised potatoes and fat mutton pieces to be ladled out in thick peppered flour, fat and sauce.[10.36]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0001.wav": "[0.29][S01] Stuff it into you, his belly counselled him.[2.81]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0002.wav": "[0.35][S01] After early nightfall, the yellow lamps would light up here and there the squalid quarter of the brothels.[6.01]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0003.wav": "[0.35][S01] Hello, bertie. Any good in your mind?[2.62]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0004.wav": "[0.15][S01] Number ten fresh nellie is waiting on you. Good night, husband.[4.99]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0005.wav": "[0.35][S01] The music came nearer, and he recalled the words, the words of shelley's fragment upon the moon, wandering companionless pale for weariness.[9.61]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0006.wav": "[0.48][S01] The dull light fell more faintly upon the page, whereon another equation began to unfold itself slowly and to spread abroad its widening tail.[10.32]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0007.wav": "[0.48][S01] A cold lucid indifference reigned in his soul.[3.98]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0008.wav": "[0.48][S01] The chaos in which his ardour extinguished itself was a cold indifferent knowledge of himself.[5.98]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0009.wav": "[0.35][S01] At most by an alms given to a beggar whose blessing he fled from, he might hope wearily to win for himself some measure of actual grace.[10.01]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0010.wav": "[0.51][S01] Well, now, ennis, i declare you have a head, and so has my stick.[3.99]", + "/Users/cj/code/handy-computer/transcribe.cpp/samples/wer/librispeech-test-clean/1089-134686-0011.wav": "[0.53][S01] On saturday mornings, when the sodality met in the chapel to recite the little office, his place was a cushioned kneeling desk at the right of the altar, from which he led his wing of boys through the responses.[12.11]" + } +} diff --git a/tests/golden/moss/moss-transcribe-diarize.manifest.json b/tests/golden/moss/moss-transcribe-diarize.manifest.json new file mode 100644 index 00000000..53253ce1 --- /dev/null +++ b/tests/golden/moss/moss-transcribe-diarize.manifest.json @@ -0,0 +1,59 @@ +{ + "schema": "transcribe-golden-manifest-v1", + "family": "moss", + "variant": "moss-transcribe-diarize", + "source_model": { + "hf_repo": "OpenMOSS-Team/MOSS-Transcribe-Diarize", + "hf_revision": "d7231bbae2587a4af278735eb765b318c4f64edd" + }, + "reference": { + "kind": "author_repo_moss", + "source": "https://github.com/OpenMOSS/MOSS-Transcribe-Diarize", + "revision": "main", + "entrypoint": "scripts/dump_reference_moss_author.py" + }, + "expected_dtype": "bfloat16", + "dtype_source": "config", + "frontend": { + "sample_rate": 16000, + "n_mels": 80, + "hop_length": 160, + "fft_size": 400, + "win_length": 400, + "window": "hann_periodic", + "normalization": "per_utterance", + "preemphasis": null, + "dither": 0.0 + }, + "tokenizer_summary": { + "type": "bpe", + "vocab_size": 151936, + "special_tokens": { + "bos": 151643, + "eos": 151645, + "pad": 151643, + "audio_pad": 151671 + } + }, + "capabilities": { + "languages": [ + "en", + "zh" + ], + "language_detection": false, + "translation": false, + "timestamps": [ + "segment" + ], + "streaming": false, + "speaker_diarization": true + }, + "tolerance_file": "tests/tolerances/moss.json", + "transcript_compare": "dediarized", + "cases": [ + { + "audio": "jfk", + "language": "auto" + } + ] +} diff --git a/tests/tolerances/moss.json b/tests/tolerances/moss.json new file mode 100644 index 00000000..fbe6bee2 --- /dev/null +++ b/tests/tolerances/moss.json @@ -0,0 +1,92 @@ +{ + "_comment": [ + "Finalized Stage-4 tolerances for MOSS-Transcribe-Diarize (moss).", + "CORRECTNESS REGIME: reference-dtype BF16 GGUF; KV cache F16; reference mel", + "(C++ MelFrontend, validated separately in Step 7); backend cpu, threads 1", + "(validate.py defaults). Every number below is measured in this regime via", + "`validate.py all` and finalized as max(1.5 x observed C++ drift, Stage-2", + "provisional magnitude budget, 1e-6).", + "DOMINANT DRIFT SOURCE: the reference runs BF16 (torch, attn_implementation", + "eager); the C++ path dequantizes BF16 weights to F32 and computes in F32", + "(F16 conv kernels with F32 accumulation). So C++ is MORE precise than the", + "reference and the gap is bf16-reference storage/compute vs f32 C++ - a", + "constant ~1-3% relative drift, not a bug. Confirmed WER-neutral: on the", + "first 100 LibriSpeech test-clean utterances the C++ ref-dtype WER (1.40%)", + "is bit-identical to the Oracle reference WER on the same subset (1.40%).", + "LARGE ABSOLUTE MAXES ARE EXPECTED, NOT BUGS: the Whisper encoder and Qwen3", + "decoder residual streams carry genuinely large pre-normalization activations", + "(reference |enc.block.23| reaches ~3.6e3, |dec.out_before_head| ~1e2). bf16", + "relative error on those single largest elements yields the big max_abs here;", + "the following LayerNorm/RMSNorm renormalizes them (enc.ln_post drift falls", + "back to ~1.8e-3 mean). The per-tensor MEAN budgets are the meaningful gate.", + "ENCODER PADDING: the encoder runs on the reference's 30s zero-padded chunk;", + "frames beyond token_len*4 (jfk: 552 of 1500) are ill-conditioned and TRIMMED", + "before the 4x merge + adaptor, so they never reach the decoder. Localized", + "host-side: real-region (0:552) and padding-region drift are the same order;", + "the huge maxes sit on large-magnitude activations, not a mask/shape bug.", + "dec.token_emb is a pure get_rows of the GGUF embedding table -> pinned exact", + "0.0 (zero-drift exception). No _provisional flags remain." + ], + "dec.token_emb": { + "max_abs": 0.0, + "mean_abs": 0.0 + }, + "enc.mel.in": { + "max_abs": 9.871853202581411e-05, + "mean_abs": 5.1495853070020074e-06, + "_comment": "C++ MelFrontend vs reference WhisperFeatureExtractor; observed drift (2.2e-5/3.4e-8) is below the Stage-2 magnitude budget, kept." + }, + "enc.pos_add.out": { + "max_abs": 0.05, + "mean_abs": 0.0022 + }, + "enc.block.0.out": { + "max_abs": 0.12, + "mean_abs": 0.004 + }, + "enc.block.23.out": { + "max_abs": 3100.0, + "mean_abs": 0.08, + "_comment": "pre-final-LN residual; ref |max| ~3.6e3, bf16 rel error dominates max_abs. Normalized away by enc.ln_post; mean is the real gate." + }, + "enc.ln_post.out": { + "max_abs": 13.0, + "mean_abs": 0.003 + }, + "enc.merge.out": { + "max_abs": 1.5, + "mean_abs": 0.003 + }, + "enc.adaptor.out": { + "max_abs": 0.4, + "mean_abs": 0.012, + "_comment": "decoder audio handoff (post-LayerNorm); rel_mean 0.96% - clean bf16 precision." + }, + "dec.audio_injected": { + "max_abs": 0.4, + "mean_abs": 0.007 + }, + "dec.block.0.out": { + "max_abs": 0.75, + "mean_abs": 0.0095 + }, + "dec.block.27.out": { + "max_abs": 320.0, + "mean_abs": 0.26, + "_comment": "pre-final-RMSNorm residual; large activations, bf16 accumulation over 28 layers. Renormalized before the head." + }, + "dec.out_before_head": { + "max_abs": 8.5, + "mean_abs": 0.08 + }, + "dec.logits_raw": { + "max_abs": 0.48, + "mean_abs": 0.072, + "_comment": "first-step (prefill) logits; argmax preserved (transcript exact) despite bf16 drift." + }, + "dec.logits_raw.gen8": { + "max_abs": 0.5, + "mean_abs": 0.065, + "_comment": "mid-generation (n_past>0) logits at greedy step 8 - KV-cache decode coverage." + } +}