diff --git a/README.md b/README.md index 19c924c49..190a9ed2f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ C/C++ speech-to-text inference library. Runs diverse STT model families via [GGU | Multitalker Parakeet Streaming 0.6B v1 | `multitalker-parakeet-streaming-0.6b-v1` | diarize, streaming, token timestamps | [docs/models/multitalker-parakeet-streaming-0.6b-v1.md](docs/models/multitalker-parakeet-streaming-0.6b-v1.md) | | Nemotron 3.5 ASR Streaming 0.6B | `nemotron-3.5-asr-streaming-0.6b` | streaming, token timestamps | [docs/models/nemotron-3.5-asr-streaming-0.6b.md](docs/models/nemotron-3.5-asr-streaming-0.6b.md) | | Nemotron Speech Streaming EN 0.6B | `nemotron-speech-streaming-en-0.6b` | streaming, token timestamps | [docs/models/nemotron-speech-streaming-en-0.6b.md](docs/models/nemotron-speech-streaming-en-0.6b.md) | -| Parakeet | `parakeet-ctc-0.6b`, `parakeet-ctc-1.1b`, `parakeet-primeline`, `parakeet-rnnt-0.6b`, `parakeet-rnnt-1.1b`, `parakeet-tdt-0.6b-v2`, `parakeet-tdt-0.6b-v3`, `parakeet-tdt-1.1b`, `parakeet-tdt_ctc-1.1b`, `parakeet-tdt_ctc-110m`, `parakeet-unified-en-0.6b` | streaming, token timestamps | [docs/models/parakeet.md](docs/models/parakeet.md) | +| Parakeet | `parakeet-ctc-0.6b`, `parakeet-ctc-1.1b`, `parakeet-primeline`, `parakeet-rnnt-0.6b`, `parakeet-rnnt-1.1b`, `parakeet-tdt-0.6b-v2`, `parakeet-tdt-0.6b-v3`, `parakeet-tdt-1.1b`, `parakeet-tdt_ctc-1.1b`, `parakeet-tdt_ctc-110m`, `parakeet-ultra`, `parakeet-unified-en-0.6b` | streaming, token timestamps | [docs/models/parakeet.md](docs/models/parakeet.md) | | Qwen3-ASR | `qwen3-asr-0.6b`, `qwen3-asr-1.7b` | - | [docs/models/qwen3-asr.md](docs/models/qwen3-asr.md) | | SenseVoice Small | `sensevoice-small` | - | [docs/models/sensevoice-small.md](docs/models/sensevoice-small.md) | | Voxtral (2507) | `voxtral-mini-3b-2507`, `voxtral-small-24b-2507` | translate | [docs/models/voxtral.md](docs/models/voxtral.md) | diff --git a/catalog/parakeet-ultra.json b/catalog/parakeet-ultra.json new file mode 100644 index 000000000..b2b43d933 --- /dev/null +++ b/catalog/parakeet-ultra.json @@ -0,0 +1,98 @@ +{ + "schema": "transcribe-catalog-v1", + "variant": "parakeet-ultra", + "family": "parakeet", + "display_name": "Parakeet Ultra", + "params": 627265543, + "license": { + "spdx": "cc-by-4.0", + "display": "CC-BY-4.0" + }, + "upstream_repo": "moondream/parakeet-ultra", + "upstream_commit": "73175eb", + "published_repo": "handy-computer/parakeet-ultra-gguf", + "docs_page": "parakeet.md", + "languages": [ + "en", "de", "fr", "es", "it", "pt", "ru", "uk", "hr", "sl", "lv", "lt", + "et", "fi", "sv", "da", "nl", "pl", "cs", "sk", "hu", "ro", "bg", "el", + "mt" + ], + "language_tag_form": "bare-bcp47", + "long_form_strategy": "chunked-unbounded", + "capabilities": { + "transcribe": {"supported":true,"verified":false}, + "translate": {"supported":false}, + "lang_detect": {"supported":true,"verified":false}, + "timestamps": {"supported":true,"verified":false,"granularities":["token"]}, + "streaming": {"supported":false}, + "diarize": {"supported":false}, + "batching": {"supported":true,"verified":false} + }, + "downloads": [ + {"quant":"F32","filename":"parakeet-ultra-F32.gguf","size_bytes":2509290240}, + {"quant":"F16","filename":"parakeet-ultra-F16.gguf","size_bytes":1256724448}, + {"quant":"Q8_0","filename":"parakeet-ultra-Q8_0.gguf","size_bytes":740363168}, + {"quant":"Q6_K","filename":"parakeet-ultra-Q6_K.gguf","size_bytes":611196832}, + {"quant":"Q5_K_M","filename":"parakeet-ultra-Q5_K_M.gguf","size_bytes":549800864}, + {"quant":"Q4_K_M","filename":"parakeet-ultra-Q4_K_M.gguf","size_bytes":486280096} + ], + "accuracy_benchmarks": [ + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"F32","metric":"wer","err_pct":1.8,"ci95":[1.65,1.95],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":732,"del":136,"ins":88},"empty_hyp":0,"utts_over_50pct":6}, + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"F16","metric":"wer","err_pct":1.8,"ci95":[1.65,1.95],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":732,"del":136,"ins":88},"empty_hyp":0,"utts_over_50pct":6}, + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":1.8,"ci95":[1.65,1.95],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":726,"del":137,"ins":92},"empty_hyp":0,"utts_over_50pct":5}, + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"Q6_K","metric":"wer","err_pct":1.81,"ci95":[1.65,1.96],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":729,"del":137,"ins":92},"empty_hyp":0,"utts_over_50pct":5}, + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"Q5_K_M","metric":"wer","err_pct":1.81,"ci95":[1.66,1.97],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":733,"del":136,"ins":90},"empty_hyp":0,"utts_over_50pct":4}, + {"dataset":"librispeech","split":"test-clean","language":"en","language_hint":"en","backend":"cuda","quant":"Q4_K_M","metric":"wer","err_pct":1.82,"ci95":[1.66,1.98],"n_utts":2620,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":738,"del":130,"ins":96},"empty_hyp":0,"utts_over_50pct":5}, + {"dataset":"fleurs","split":"test","language":"en","language_hint":"en","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":4.26,"ci95":[3.84,4.69],"n_utts":647,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":435,"del":125,"ins":60},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"de","language_hint":"de","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":4.58,"ci95":[4.17,4.98],"n_utts":862,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":660,"del":117,"ins":81},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"fr","language_hint":"fr","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":4.66,"ci95":[4.21,5.12],"n_utts":676,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":664,"del":119,"ins":56},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"es","language_hint":"es","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":3.06,"ci95":[2.75,3.4],"n_utts":908,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":533,"del":112,"ins":68},"empty_hyp":0,"utts_over_50pct":1}, + {"dataset":"fleurs","split":"test","language":"it","language_hint":"it","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":2.34,"ci95":[2.07,2.62],"n_utts":865,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":378,"del":67,"ins":53},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"pt","language_hint":"pt","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":4.16,"ci95":[3.71,4.66],"n_utts":919,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":629,"del":118,"ins":147},"empty_hyp":0,"utts_over_50pct":4}, + {"dataset":"fleurs","split":"test","language":"ru","language_hint":"ru","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":5.63,"ci95":[5.2,6.12],"n_utts":775,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":724,"del":78,"ins":49},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"uk","language_hint":"uk","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":5.71,"ci95":[5.2,6.25],"n_utts":750,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":665,"del":93,"ins":63},"empty_hyp":0,"utts_over_50pct":1}, + {"dataset":"fleurs","split":"test","language":"hr","language_hint":"hr","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":11.3,"ci95":[10.55,12.1],"n_utts":914,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1627,"del":220,"ins":159},"empty_hyp":0,"utts_over_50pct":10}, + {"dataset":"fleurs","split":"test","language":"sl","language_hint":"sl","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":18.98,"ci95":[18.08,20.04],"n_utts":834,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":2512,"del":304,"ins":319},"empty_hyp":0,"utts_over_50pct":22}, + {"dataset":"fleurs","split":"test","language":"lv","language_hint":"lv","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":18.04,"ci95":[17.22,18.93],"n_utts":851,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":2451,"del":165,"ins":165},"empty_hyp":0,"utts_over_50pct":14}, + {"dataset":"fleurs","split":"test","language":"lt","language_hint":"lt","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":17.06,"ci95":[16.38,17.78],"n_utts":986,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":2610,"del":205,"ins":108},"empty_hyp":0,"utts_over_50pct":14}, + {"dataset":"fleurs","split":"test","language":"et","language_hint":"et","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":14.44,"ci95":[13.68,15.17],"n_utts":893,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1588,"del":194,"ins":367},"empty_hyp":0,"utts_over_50pct":9}, + {"dataset":"fleurs","split":"test","language":"fi","language_hint":"fi","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":11.14,"ci95":[10.44,11.87],"n_utts":918,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1326,"del":177,"ins":145},"empty_hyp":0,"utts_over_50pct":9}, + {"dataset":"fleurs","split":"test","language":"sv","language_hint":"sv","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":12.83,"ci95":[12.09,13.6],"n_utts":759,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1544,"del":274,"ins":156},"empty_hyp":0,"utts_over_50pct":9}, + {"dataset":"fleurs","split":"test","language":"da","language_hint":"da","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":15.98,"ci95":[15.28,16.63],"n_utts":930,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":2565,"del":386,"ins":270},"empty_hyp":1,"utts_over_50pct":12}, + {"dataset":"fleurs","split":"test","language":"nl","language_hint":"nl","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":6.7,"ci95":[5.99,7.4],"n_utts":364,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":419,"del":72,"ins":72},"empty_hyp":0,"utts_over_50pct":0}, + {"dataset":"fleurs","split":"test","language":"pl","language_hint":"pl","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":6.16,"ci95":[5.64,6.69],"n_utts":758,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":737,"del":90,"ins":59},"empty_hyp":0,"utts_over_50pct":2}, + {"dataset":"fleurs","split":"test","language":"cs","language_hint":"cs","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":11.04,"ci95":[10.21,11.9],"n_utts":723,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1282,"del":126,"ins":104},"empty_hyp":0,"utts_over_50pct":17}, + {"dataset":"fleurs","split":"test","language":"sk","language_hint":"sk","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":7.83,"ci95":[7.17,8.49],"n_utts":792,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":968,"del":147,"ins":78},"empty_hyp":1,"utts_over_50pct":7}, + {"dataset":"fleurs","split":"test","language":"hu","language_hint":"hu","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":13.09,"ci95":[12.41,13.8],"n_utts":905,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1791,"del":224,"ins":220},"empty_hyp":0,"utts_over_50pct":5}, + {"dataset":"fleurs","split":"test","language":"ro","language_hint":"ro","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":10.12,"ci95":[9.48,10.78],"n_utts":883,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1647,"del":314,"ins":145},"empty_hyp":0,"utts_over_50pct":7}, + {"dataset":"fleurs","split":"test","language":"bg","language_hint":"bg","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":11.04,"ci95":[10.32,11.88],"n_utts":658,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":1227,"del":224,"ins":114},"empty_hyp":0,"utts_over_50pct":3}, + {"dataset":"fleurs","split":"test","language":"el","language_hint":"el","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":32.25,"ci95":[31.18,33.35],"n_utts":650,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":4538,"del":211,"ins":145},"empty_hyp":0,"utts_over_50pct":62}, + {"dataset":"fleurs","split":"test","language":"mt","language_hint":"mt","backend":"cuda","quant":"Q8_0","metric":"wer","err_pct":17.01,"ci95":[16.28,17.73],"n_utts":926,"batch_size":8,"timestamps":"none","engine_sha":"021aa17d","publication_profile":"asr-publication-v2","measured_on":null,"errors":{"sub":3050,"del":466,"ins":265},"empty_hyp":0,"utts_over_50pct":9} + ], + "headline_benchmark": { + "dataset": "librispeech", + "split": "test-clean", + "language": "en", + "metric": "wer", + "batch_size": 8, + "timestamps": "none" + }, + "speed_benchmarks": [ + {"machine":"ryzen-4750u","backend":"cpu","quant":"Q4_K_M","sample":"dots","sample_duration_s":35.332,"total_ms":3177.5,"xrt_compute":11.12,"wall_ms":3203.3,"xrt_wall":11.03,"load_ms":712.3,"mel_ms":66.8,"encode_ms":2769.1,"decode_ms":341.6,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"cpu","quant":"Q4_K_M","sample":"jfk","sample_duration_s":11.0,"total_ms":778.4,"xrt_compute":14.13,"wall_ms":787.3,"xrt_wall":13.97,"load_ms":708.9,"mel_ms":18.2,"encode_ms":687.9,"decode_ms":72.2,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"cpu","quant":"Q8_0","sample":"dots","sample_duration_s":35.332,"total_ms":3054.8,"xrt_compute":11.57,"wall_ms":3080.2,"xrt_wall":11.47,"load_ms":886.8,"mel_ms":63.3,"encode_ms":2657.4,"decode_ms":334.0,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"cpu","quant":"Q8_0","sample":"jfk","sample_duration_s":11.0,"total_ms":732.7,"xrt_compute":15.01,"wall_ms":741.3,"xrt_wall":14.84,"load_ms":883.3,"mel_ms":18.0,"encode_ms":641.7,"decode_ms":72.9,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"vulkan","quant":"Q4_K_M","sample":"dots","sample_duration_s":35.332,"total_ms":1639.7,"xrt_compute":21.55,"wall_ms":1667.1,"xrt_wall":21.19,"load_ms":365.5,"mel_ms":59.3,"encode_ms":1251.9,"decode_ms":328.6,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"vulkan","quant":"Q4_K_M","sample":"jfk","sample_duration_s":11.0,"total_ms":458.8,"xrt_compute":23.98,"wall_ms":473.4,"xrt_wall":23.24,"load_ms":375.4,"mel_ms":17.8,"encode_ms":369.5,"decode_ms":71.5,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"vulkan","quant":"Q8_0","sample":"dots","sample_duration_s":35.332,"total_ms":1619.3,"xrt_compute":21.82,"wall_ms":1649.3,"xrt_wall":21.42,"load_ms":462.9,"mel_ms":65.7,"encode_ms":1226.8,"decode_ms":326.7,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"ryzen-4750u","backend":"vulkan","quant":"Q8_0","sample":"jfk","sample_duration_s":11.0,"total_ms":455.0,"xrt_compute":24.18,"wall_ms":469.7,"xrt_wall":23.42,"load_ms":455.3,"mel_ms":17.7,"encode_ms":365.7,"decode_ms":71.5,"engine_sha":"08f90613","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"cpu","quant":"Q4_K_M","sample":"dots","sample_duration_s":35.332,"total_ms":1115.4,"xrt_compute":31.68,"wall_ms":1118.4,"xrt_wall":31.59,"load_ms":188.6,"mel_ms":2.3,"encode_ms":1065.8,"decode_ms":47.3,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"cpu","quant":"Q4_K_M","sample":"jfk","sample_duration_s":11.0,"total_ms":310.6,"xrt_compute":35.42,"wall_ms":312.1,"xrt_wall":35.24,"load_ms":174.6,"mel_ms":0.9,"encode_ms":298.6,"decode_ms":11.1,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"cpu","quant":"Q8_0","sample":"dots","sample_duration_s":35.332,"total_ms":1132.3,"xrt_compute":31.2,"wall_ms":1135.5,"xrt_wall":31.12,"load_ms":230.4,"mel_ms":2.3,"encode_ms":1081.8,"decode_ms":48.2,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"cpu","quant":"Q8_0","sample":"jfk","sample_duration_s":11.0,"total_ms":283.0,"xrt_compute":38.87,"wall_ms":284.9,"xrt_wall":38.61,"load_ms":227.3,"mel_ms":0.9,"encode_ms":271.0,"decode_ms":11.2,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"metal","quant":"Q4_K_M","sample":"dots","sample_duration_s":35.332,"total_ms":212.4,"xrt_compute":166.34,"wall_ms":217.1,"xrt_wall":162.77,"load_ms":144.5,"mel_ms":2.3,"encode_ms":161.7,"decode_ms":48.4,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"metal","quant":"Q4_K_M","sample":"jfk","sample_duration_s":11.0,"total_ms":54.3,"xrt_compute":202.43,"wall_ms":57.4,"xrt_wall":191.54,"load_ms":148.4,"mel_ms":0.8,"encode_ms":42.6,"decode_ms":10.9,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"metal","quant":"Q8_0","sample":"dots","sample_duration_s":35.332,"total_ms":206.2,"xrt_compute":171.31,"wall_ms":210.9,"xrt_wall":167.51,"load_ms":194.5,"mel_ms":2.3,"encode_ms":156.8,"decode_ms":47.1,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null}, + {"machine":"m4-max","backend":"metal","quant":"Q8_0","sample":"jfk","sample_duration_s":11.0,"total_ms":53.2,"xrt_compute":206.94,"wall_ms":56.3,"xrt_wall":195.36,"load_ms":193.5,"mel_ms":0.8,"encode_ms":41.3,"decode_ms":11.1,"engine_sha":"a91a58db","publication_profile":"asr-publication-v2","measured_on":"2026-10-05","thermal_gated":null} + ] +} diff --git a/docs/models/parakeet-ultra.md b/docs/models/parakeet-ultra.md new file mode 100644 index 000000000..972a4fa17 --- /dev/null +++ b/docs/models/parakeet-ultra.md @@ -0,0 +1,251 @@ +# Parakeet Ultra + + +Upstream: [`moondream/parakeet-ultra`](https://huggingface.co/moondream/parakeet-ultra) at [`73175eb`](https://huggingface.co/moondream/parakeet-ultra/commit/73175eb). + +Moondream's post-trained parakeet-tdt-0.6b-v3. Offline multilingual +speech-to-text over the same 25 European languages, with lower error rates +than v3 on English and on every FLEURS language we measured. A FastConformer +encoder with a TDT transducer decoder, plus a small voice-activity head that +the model uses to cut long recordings at pauses into segments of at most +30 seconds. Takes 16 kHz mono WAV and produces a punctuated, cased +transcript with optional token-level timestamps. A drop-in replacement for +v3: same size, languages and API. Not a streaming model and does not +translate. + + +## What it's for + +Offline multilingual speech-to-text, and the default pick over +`parakeet-tdt-0.6b-v3` for new work. Moondream post-trained v3 without +changing its architecture, tokenizer or frontend, so it covers the same 25 +European languages and drops into any v3 setup by swapping the GGUF. + +The one addition is a small voice-activity head on the encoder's input +layers. The model uses it to segment long audio, described below. It is not +a streaming model and does not translate. + +See Moondream's [model card](https://huggingface.co/moondream/parakeet-ultra) +for training details and their own evaluation. + +## Long-form audio + +Audio up to 30 seconds is transcribed in one pass, exactly like v3. Longer +audio is segmented the way Moondream's runtime does it: + +1. The voice-activity head scores every 80 ms frame, scanning the file in + 120 s blocks. +2. Frames above 0.5 are speech; gaps under 0.1 s are bridged and runs under + 0.1 s dropped. A pause is any silence of at least 0.2 s. +3. Each segment ends at the midpoint of the last pause that falls between + 1 s and 30 s into it, or at 30 s if there is none. Segments without speech + are skipped. +4. Each segment is transcribed independently; the texts are joined with a + space and timestamps are shifted to file time. + +The thresholds are stored in the GGUF, not hard-coded. Cut points match +Moondream's runtime sample for sample. Two consequences when moving from v3: +segment boundaries and timestamps on long files differ from v3's output, +and audio over 30 s pays for one extra voice-activity pass. + + +Licensed CC-BY-4.0. Ported from upstream commit [`73175eb`](https://huggingface.co/moondream/parakeet-ultra/commit/73175eb), pinned 2026-10-04. Validated against the kestrel 0.9.1 (Photon) reference at transcribe.cpp commit [`08f9e6f0`](https://github.com/handy-computer/transcribe.cpp/tree/08f9e6f0) on 2026-10-05. + + +## Download + + +| Quantization | Download | Size | WER (LibriSpeech test-clean) | +| --- | --- | ---: | ---: | +| F32 | [parakeet-ultra-F32.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-F32.gguf) | 2.51 GB | 1.80% | +| F16 | [parakeet-ultra-F16.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-F16.gguf) | 1.26 GB | 1.80% | +| Q8_0 | [parakeet-ultra-Q8_0.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-Q8_0.gguf) | 740 MB | 1.80% | +| Q6_K | [parakeet-ultra-Q6_K.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-Q6_K.gguf) | 611 MB | 1.81% | +| Q5_K_M | [parakeet-ultra-Q5_K_M.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-Q5_K_M.gguf) | 550 MB | 1.81% | +| Q4_K_M | [parakeet-ultra-Q4_K_M.gguf](https://huggingface.co/handy-computer/parakeet-ultra-gguf/resolve/main/parakeet-ultra-Q4_K_M.gguf) | 486 MB | 1.82% | + + + +WER on the full LibriSpeech test-clean split (2,620 utterances), batch size 8, timestamps none, language hint `en`, decoded on cuda. Measured at transcribe.cpp `021aa17d`. + + + +Greedy transducer decoding, no external LM. The reference is Moondream's +own runtime (kestrel 0.9.1, the engine behind Photon), not transformers. +On this manifest kestrel scores 1.80% and the C++ F32 build matches it. + +Moondream's card reports 1.41% on the same split, scored with the Open +ASR Leaderboard normalizer. + +Long-form: on 11 full TED-LIUM 3 talks, segmented by the model's +voice-activity head exactly as kestrel does it, F32 scores 2.22%, identical +to kestrel, and every quant stays within 0.05pp. + + + +**FLEURS test** + +| Language | Metric | Q8_0 | +| --- | --- | ---: | +| bg | WER | 11.04% | +| cs | WER | 11.04% | +| da | WER | 15.98% | +| de | WER | 4.58% | +| el | WER | 32.25% | +| en | WER | 4.26% | +| es | WER | 3.06% | +| et | WER | 14.44% | +| fi | WER | 11.14% | +| fr | WER | 4.66% | +| hr | WER | 11.30% | +| hu | WER | 13.09% | +| it | WER | 2.34% | +| lt | WER | 17.06% | +| lv | WER | 18.04% | +| mt | WER | 17.01% | +| nl | WER | 6.70% | +| pl | WER | 6.16% | +| pt | WER | 4.16% | +| ro | WER | 10.12% | +| ru | WER | 5.63% | +| sk | WER | 7.83% | +| sl | WER | 18.98% | +| sv | WER | 12.83% | +| uk | WER | 5.71% | + + +The voice-activity head is kept at F32 in every quant. Lower quants can still +move a cut point, because the head reads the quantized input layers' output: +on a 306 s test file, F16, Q8_0 and Q6_K cut exactly where F32 does, Q5_K_M +differs at one segment boundary and Q4_K_M produces 12 segments instead of +11. + +## Quick Start + +```bash +cmake -B build +cmake --build build + +build/bin/transcribe-cli \ + -m models/parakeet-ultra/parakeet-ultra-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 +``` + +## Performance + +### Apple M4 Max + + +Compute latency (mel + encode + decode), speedup over realtime in parentheses; profile `asr-publication-v2`: mean over 3 iterations after 1 warmup. + +| Backend | Sample | Q8_0 | Q4_K_M | +| ------- | ------------ | ---------------: | ---------------: | +| Metal | jfk (11.0s) | 53 ms (206.94×) | 54 ms (202.43×) | +| Metal | dots (35.3s) | 206 ms (171.31×) | 212 ms (166.34×) | +| CPU | jfk (11.0s) | 283 ms (38.87×) | 311 ms (35.42×) | +| CPU | dots (35.3s) | 1.13 s (31.20×) | 1.12 s (31.68×) | + +Apple M4 Max: transcribe.cpp `a91a58db` on 2026-10-05. + + +### AMD Ryzen 7 PRO 4750U + + +Compute latency (mel + encode + decode), speedup over realtime in parentheses; profile `asr-publication-v2`: mean over 3 iterations after 1 warmup. + +| Backend | Sample | Q8_0 | Q4_K_M | +| ------- | ------------ | --------------: | --------------: | +| Vulkan | jfk (11.0s) | 455 ms (24.18×) | 459 ms (23.98×) | +| Vulkan | dots (35.3s) | 1.62 s (21.82×) | 1.64 s (21.55×) | +| CPU | jfk (11.0s) | 733 ms (15.01×) | 778 ms (14.13×) | +| CPU | dots (35.3s) | 3.05 s (11.57×) | 3.18 s (11.12×) | + +AMD Ryzen 7 PRO 4750U (Radeon RADV RENOIR): transcribe.cpp `08f90613` on 2026-10-05. + + +Benchmark reproduction: + +```bash +uv run scripts/bench/run.py --profile --models parakeet-ultra +``` + +## Numerical Validation + +transcribe.cpp is validated tensor-by-tensor against kestrel 0.9.1, the +runtime Moondream ships for this model, on three cases: `jfk` (encoder and +decode), and `dots` (35 s) and `dots-full` (306 s) through the long-form path. +The long-form cases compare every voice-activity scan block, the cut points +(exact), each segment's encoder output and the stitched transcript. All +transcripts match kestrel verbatim, and LibriSpeech test-clean and TED-LIUM +long-form WER at F32 equal kestrel's exactly. + +| Field | Value | +| --- | --- | +| Reference | kestrel 0.9.1 / kestrel-kernels 0.7.4, `moondream/parakeet-ultra` | +| Dump script | `scripts/dump_reference_parakeet_kestrel.py` | +| Manifest | `tests/golden/parakeet/parakeet-ultra.manifest.json` | +| Tolerances | `tests/tolerances/parakeet-ultra.json` | +| Command | `uv run scripts/validate.py all --family parakeet --variant parakeet-ultra` | + +The tensor comparison runs with `TRANSCRIBE_NO_FLASH=1` (set by the manifest), +because the flash-attention path rounds one attention term to F16. Flash +attention stays on in production; WER is the check there. Remaining drift is +float32 accumulation order, plus float32 STFT noise on kestrel's side of the +mel comparison. + +To match kestrel, the model takes three runtime paths that the other parakeet +variants don't, each switched on by a GGUF key: masking after every strided +subsampling conv, F32 for the subsampler's pointwise convs, and a single +per-utterance symbol budget in the TDT decoder. + +## Reproduction + +### Convert + +Moondream ships HF safetensors only; the converter maps them onto the NeMo +layout and adds the voice-activity head and segmenter constants. + +```bash +uv run --project scripts/envs/parakeet \ + scripts/convert-parakeet.py moondream/parakeet-ultra +``` + +### Quantize + +```bash +uv run scripts/quantize-all.py \ + models/parakeet-ultra/parakeet-ultra-F32.gguf +``` + +### Validate + +```bash +uv run scripts/validate.py all --family parakeet --variant parakeet-ultra +``` + +### WER + +```bash +uv run scripts/wer/ingest.py librispeech +uv run scripts/wer/ingest.py tedlium-longform + +# reference arm (kestrel) +uv run --project scripts/envs/parakeet-kestrel \ + scripts/wer/run_reference_parakeet_kestrel.py \ + --manifest samples/wer/tedlium-longform.manifest.jsonl \ + --out reports/wer/parakeet-ultra-REF.tedlium-longform.jsonl + +# transcribe.cpp arm +uv run scripts/wer/run.py \ + --model models/parakeet-ultra/parakeet-ultra-F32.gguf \ + --manifest samples/wer/tedlium-longform.manifest.jsonl \ + --out reports/wer/parakeet-ultra-F32.tedlium-longform.b1.jsonl + +uv run scripts/wer/score.py .jsonl +``` diff --git a/docs/models/parakeet.md b/docs/models/parakeet.md index 23588c03a..1e43bfd55 100644 --- a/docs/models/parakeet.md +++ b/docs/models/parakeet.md @@ -4,10 +4,10 @@ NVIDIA's [Parakeet](https://huggingface.co/collections/nvidia/parakeet) family ported to transcribe.cpp. A FastConformer encoder paired with one of three decoder heads — TDT (transducer with a duration prediction head), classical RNN-T, or CTC — and a TDT+CTC hybrid that ships both -heads in one checkpoint. All variants take 16 kHz mono PCM through an -80-bin mel frontend; English-only across the family, except -`parakeet-tdt-0.6b-v3` and its German fine-tune `parakeet-primeline`, -which cover 25 European languages. +heads in one checkpoint. All variants take 16 kHz mono PCM through a +mel frontend; English-only across the family, except +`parakeet-tdt-0.6b-v3`, Moondream's post-trained `parakeet-ultra` and the +German fine-tune `parakeet-primeline`, which cover 25 European languages. For the architecture deep-dive, validation contract, and porting notes, see the family doc at @@ -19,9 +19,11 @@ Most users want one of three: - **English transcription → `parakeet-tdt-0.6b-v2`.** The default pick: small, fast, near top-of-family accuracy on English. -- **Multilingual (25 European languages) → `parakeet-tdt-0.6b-v3`.** The - only multilingual variant; same size as v2, broader coverage at a - small English-WER cost. +- **Multilingual (25 European languages) → `parakeet-ultra`.** Moondream's + post-trained v3: same size, languages and API, more accurate on English + and on every FLEURS language. A drop-in replacement for + `parakeet-tdt-0.6b-v3`, which remains available. + See [parakeet-ultra.md](parakeet-ultra.md) for how it segments long audio. - **German → `parakeet-primeline`.** primeLine's German fine-tune of v3. Same size and speed; tuned for German while keeping the other 24 v3 languages usable. @@ -52,10 +54,11 @@ WER is on LibriSpeech test-clean for the **Q8_0** preset, measured by transcribe.cpp's WER pipeline. See each per-variant doc for the full quant matrix and the comparison to NVIDIA's self-reported numbers. - + | Variant | Params | Languages | Q8_0 size | Benchmark | Q8_0 | Capabilities | Doc | | --- | ---: | --- | ---: | --- | ---: | --- | --- | | `parakeet-tdt-0.6b-v2` | 618M | en | 730 MB | LibriSpeech test-clean (WER) | 1.69% | token timestamps | [parakeet-tdt-0.6b-v2.md](parakeet-tdt-0.6b-v2.md) | +| `parakeet-ultra` | 627M | 25 languages + auto-detect | 740 MB | LibriSpeech test-clean (WER) | 1.80% | token timestamps | [parakeet-ultra.md](parakeet-ultra.md) | | `parakeet-tdt-0.6b-v3` | 627M | 25 languages + auto-detect | 740 MB | LibriSpeech test-clean (WER) | 1.94% | token timestamps | [parakeet-tdt-0.6b-v3.md](parakeet-tdt-0.6b-v3.md) | | `parakeet-primeline` | 627M | 25 languages + auto-detect | 740 MB | FLEURS de (WER) | 5.98% | token timestamps | [parakeet-primeline.md](parakeet-primeline.md) | | `parakeet-tdt-1.1b` | 1.1B | en | 1.27 GB | LibriSpeech test-clean (WER) | 1.38% | token timestamps | [parakeet-tdt-1.1b.md](parakeet-tdt-1.1b.md) | @@ -80,7 +83,9 @@ each per-variant doc has direct download links. No practical per-call length limit (`transcribe_capabilities.max_audio_ms == 0`): the Conformer encoder's positional encoding is recomputed per call, so audio of -any length is processed in a single pass — pass arbitrarily long recordings. See +any length is processed in a single pass — pass arbitrarily long recordings. The +exception is `parakeet-ultra`, which cuts audio over 30 s into segments at pauses +found by its voice-activity head; length is still unbounded. See the [input-length contract](../input-limits.md). ## Quick start @@ -118,8 +123,10 @@ for the per-config WER and the `--stream-buf-{left,chunk,right}-ms` CLI surface. Other Parakeet variants run offline only. What's not supported (consistent across the family): translation, -VAD, speaker diarization. Language coverage is English-only except -`parakeet-tdt-0.6b-v3` and `parakeet-primeline` (25 European languages, +speaker diarization, and VAD as a user-facing feature (`parakeet-ultra` uses +its voice-activity head internally, only to segment long audio). Language +coverage is English-only except `parakeet-tdt-0.6b-v3`, `parakeet-ultra` and +`parakeet-primeline` (25 European languages, auto-detected; the language hint is accepted but does not change the output). Note that the v3 lineage, including `parakeet-primeline`, writes German `ss` where standard diff --git a/docs/porting/families/parakeet.md b/docs/porting/families/parakeet.md index b6cb4dce5..955668829 100644 --- a/docs/porting/families/parakeet.md +++ b/docs/porting/families/parakeet.md @@ -160,6 +160,43 @@ Allowed statuses: `PASS` | `SKIP — not exposed by runtime` | | multitalker-parakeet-streaming-0.6b-v1 | Speaker diarization (produce speaker turns) | diarization | same bundle smoke; inspect `transcribe_n_speaker_segments` | non-empty 1-based speaker turns, independent of transcript rows | PASS on bundle GGUFs — embedded Sortformer predictions populate speaker segments; plain GGUFs intentionally retain the single-speaker capability surface. | | all variants | Word timestamps | only if exposed | `transcribe-cli --timestamps word -m ` (any variant) | per-word `t0_ms`/`t1_ms` in JSON output | PASS — derived host-side from emit-frame indices (TDT/RNNT) or per-frame argmax (CTC); same code path as the existing v2/v3 word-timestamp gate, no per-variant differences | +### parakeet-ultra (moondream/parakeet-ultra) + +Post-trained v3 (same arch/tokenizer/frontend) plus a `vad_head` on the +subsampler that the publisher's runtime (Photon/kestrel) uses to cut long +audio at pauses into <=30 s segments. Reference is kestrel 0.9.1 for +everything (frontend, ASR, VAD head, segmenter); transformers is not used. Intake: +`reports/porting/parakeet/parakeet-ultra/intake.json`. + +Oracle (env `scripts/envs/parakeet-kestrel`, selected by the manifest's +`reference.env`). jfk runs `encoder` + `decode`; `dots` (35 s) and +`dots-full` (306 s) run `longform`, which records every VAD scan block, +cut point and per-segment decode in `segments.json`: + +```bash +uv run scripts/validate.py ref --family parakeet --variant parakeet-ultra +uv run --project scripts/envs/parakeet-kestrel scripts/wer/run_reference_parakeet_kestrel.py \ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \ + --out reports/wer/parakeet-ultra-REF.librispeech-test-clean.jsonl +uv run scripts/wer/ingest.py tedlium-longform # 11 TED-LIUM 3 talks +uv run --project scripts/envs/parakeet-kestrel scripts/wer/run_reference_parakeet_kestrel.py \ + --manifest samples/wer/tedlium-longform.manifest.jsonl \ + --out reports/wer/parakeet-ultra-REF.tedlium-longform.jsonl +``` + +| Capability | Mode | Command / test | Expected observable | Target | Status | +|---|---|---|---|---|---| +| Transcribe | explicit en | `build/bin/transcribe-cli -m models/parakeet-ultra/parakeet-ultra-F32.gguf --language en samples/jfk.wav` | English transcript with PnC; LibriSpeech test-clean WER within Stage 7 band of the kestrel oracle | MUST PASS | PASS — jfk byte-equal to kestrel; LibriSpeech test-clean 1.8028% = kestrel REF 1.8028% (2620 utts) | +| Transcribe | auto | `build/bin/transcribe-cli -m models/parakeet-ultra/parakeet-ultra-F32.gguf samples/jfk.wav` | English transcript, no hint | MUST PASS | PASS — no-hint jfk byte-equal to kestrel | +| Transcribe | explicit non-English (de/es FLEURS clip) | `… --language de ` | transcript in source language | MUST PASS | PASS — german.wav and fleurs-es-1001146817223348054 byte-equal to kestrel, with and without --language | +| Offline batch | batch | `uv run scripts/batch_parity.py --model --list --golden-in tests/golden/batch/parakeet-ultra.cpu.json --batch-sizes 2,4,8 --backend cpu` | per-item transcripts equal to serial | MUST PASS | PASS — run_batch parallel encoder (kestrel masks + per-utterance extent masks); text byte-equal serial vs 2/4/8 on 13 clips incl. 5 long-form (golden tests/golden/batch/parakeet-ultra.cpu.json); same-length CPU tensor parity bit-exact; batch-8 LibriSpeech hyps identical to batch-1 (1.8028%). Batches containing a >30 s clip run per utterance | +| Timestamps (token/word/segment) | output | `… --timestamps word samples/jfk.wav` | monotonic word times, same TDT duration math as v3 | MUST PASS | PASS — word times identical to kestrel (jfk 22, dots 103, dots-full 809 words, 10 ms print precision), monotonic, shifted by segment start on long-form | +| VAD head (`vad_head.*`) speech probabilities | tensor parity | dump `vad.prob` per 80 ms frame vs kestrel `ParakeetTdt.speech_probabilities` (120 s block scan) on long clips | per-frame parity; identical speech regions after threshold 0.5 / 0.1 s bridge / 0.1 s min run | MUST PASS | PASS — vad.b.{mel,pre_encode,proj,ctx,prob} within finalized tolerances on 4 blocks; vad.prob max_abs 1.9e-6 | +| Long-form pause segmentation (>30 s) | long audio | TED-LIUM 3 eleven-talk set, one `transcribe_run` per talk | segment boundaries (sample indices) identical to kestrel `pause_segments` with the VAD head as pause source; stitched text and shifted timestamps match kestrel; WER within Stage 7 band of the kestrel oracle | MUST PASS | PASS — longform.segments exact (0.0) on dots (2 segs) and dots-full (11 segs, 3 scan blocks); stitched transcripts exact; TED-LIUM long-form 2.2195% = kestrel REF 2.2195% (11/11 hyps identical); segmenter unit test vs kestrel fixtures | +| Streaming | streaming | n/a (offline full-context model, `streaming: false`) | n/a | OUT OF SCOPE — no streaming training; publisher's live mode is window re-decoding, not cache-aware | SKIP — not exposed by runtime (offline-only model) | +| Speaker diarization | n/a | n/a | n/a | OUT OF SCOPE — not a diarizer | SKIP — not exposed by runtime | +| Translation | n/a | n/a | n/a | OUT OF SCOPE — not advertised | SKIP — not exposed by runtime | + ## Open decisions before Stage 3 (convert) These decisions block converter design for the new variants and should @@ -266,11 +303,55 @@ overrides; non-NeMo families are unaffected. unified, ctc) are not locked at intake time; they are read from the archive during Stage 3 convert. Each intake's `intake_gaps` enumerates this. +- Two F16 roundings found while porting `parakeet-ultra` apply to every + parakeet variant but are fixed or bypassed for ultra only: the vendored + `ggml_conv_2d` im2cols the subsampler's pointwise convs to F16 (~3e-4 rel + at `enc.pre_encode.out`; ultra opts into `ConvPolicy::pre_encode_f32_pointwise`), + and flash attention casts the rel-pos score bias to F16 (~1e-3 rel at + `enc.final`; ultra's tensor regime sets `TRANSCRIBE_NO_FLASH=1`). The other + variants' tolerances were measured with both in place. Before tightening + them, enable the F32 pointwise path and the no-flash regime and re-validate. + See the forward map's ultra notes. ## Stage 3 conversion notes Per-variant decisions surfaced during Stage 3 (`porting-3-convert`). +### `parakeet-ultra` + +- **HF-safetensors input path** — moondream ships only `config.json` + + `model.safetensors` + `tokenizer.json` (no `.nemo`). + `load_hf_safetensors_model()` renames HF keys to NeMo keys through an + explicit rule table (an unmapped name is an error), builds a NeMo-shaped + cfg from `config.json` (asserting silu FF, relu joint, k=3/s=2 + subsampling, no attention/conv bias), and hands the shared NeMo write + path the same surface `_DirectNemoArchive` does. Key renames: + `subsampling.layers.N`→`pre_encode.conv.N`, `subsampling.linear`→ + `pre_encode.out`, `{q,k,v,o}_proj`→`linear_{q,k,v,out}`, + `relative_k_proj`→`linear_pos`, `bias_{u,v}`→`pos_bias_{u,v}`, + `conv.norm`→`conv.batch_norm`, `encoder_projector`→`joint.enc`, + `decoder.decoder_projector`→`joint.pred`, `joint.head`→`joint.joint_net.2`. +- **F16 at rest → F32 GGUF** — 356 F16 tensors are upcast (exact). The + F32 GGUF is the parity artifact; F16 loses nothing at rest. +- **Tokenizer from `tokenizer.json`** — `_HfBpeAsSpm` rebuilds the + SentencePiece surface: scores are 0 for the 274 user-defined pieces and + `-(id-274)` after; every non-unk piece NORMAL. The emitted + tokens/scores/types equal the v3 `.nemo` tokenizer.model exactly + (8193 entries incl. ``, checked against the cached v3 archive). +- **VAD head** — emitted as `vad.{proj,ctx,out}.{weight,bias}`, PyTorch + `[out, in, k]` layout. `policy.cpp` and `reference_dtype_for` route + `vad.*.weight` to the Conv bucket, so the head stays F32 in every quant + preset (0.2M params; quantizing it could move cut points). +- **Segmenter constants in the GGUF** — kestrel's VAD threshold (0.5), + min speech/gap (0.1 s), min pause (0.2 s), segment cap (30 s), min + segment (1 s) and scan block (120 s) are written as + `stt.parakeet.vad.*` / `stt.parakeet.segmenter.*` so the C++ segmenter + reads them rather than hard-coding a second copy. +- **Frontend** — `stt.frontend.dither = 0` (kestrel never dithers; the C++ + frontend reads but does not apply dither). Window `hann` (symmetric, as + NeMo and kestrel use). +- **Languages** — same 25 as v3, written in the moondream card's order. + ### `parakeet-tdt-1.1b` - **VARIANT_PROFILES dispatch** — keying by `decoder.vocab_size` was diff --git a/reports/porting/parakeet/forward-map.md b/reports/porting/parakeet/forward-map.md index d58db4e65..511ed6653 100644 --- a/reports/porting/parakeet/forward-map.md +++ b/reports/porting/parakeet/forward-map.md @@ -127,3 +127,9 @@ cache to mis-index. - **`nemotron-speech-streaming-en-0.6b`**: 24 layers, d_model=1024, **n_mels=128**, `use_bias=false`, RNNT head, pred_n_layers=2. **First variant exercising `att_context_style="chunked_limited"`, causal depthwise conv (`conv_context_left=8, conv_context_right=0` for kernel=9), `conv_norm_type="layer_norm"`, and `fe_normalize="none"` (NeMo's `normalize="NA"` no-op, canonicalised at convert time).** Frontend feature stats (per-feature mean/std) are baked into training rather than computed at inference, so the C++ mel frontend emits unnormalised log-mel and the loader accepts `normalize="none"` alongside `"per_feature"`. Streaming runtime knobs (lookahead, encoder chunk size, decoder context) are deferred to the streaming bring-up — offline-only in v1, same as `unified-en-0.6b`. - **`multitalker-parakeet-streaming-0.6b-v1`**: 24 layers, d_model=1024, **n_mels=128**, `use_bias=false`, RNNT head, pred_n_layers=2. Same cache-aware streaming FastConformer as `nemotron-speech-streaming-en-0.6b` (chunked_limited `[70,13]`, causal depthwise conv, layer_norm conv, `fe_normalize="none"`); **first and only variant exercising the always-on layer-0 speaker-kernel FF injection** (see the Deviations bullet). Ported in SINGLE-SPEAKER mode; the full multitalker path (external Sortformer diarization, N-instance orchestration, SegLST) is OUT OF SCOPE (see the multitalker integration brief). The +8 speaker-kernel tensors are the only weight-set delta from the English predecessor. - **`nemotron-3.5-asr-streaming-0.6b`**: 24 layers, d_model=1024, **n_mels=128**, `use_bias=false`, RNNT head, pred_n_layers=2. Same FastConformer geometry as the English predecessor; **first variant exercising the multilingual prompt-conditioning MLP** (`EncDecRNNTBPEModelWithPrompt`, num_prompts=128, prompt_hidden read from the GGUF; encoder output passes through `prompt_kernel` before the RNN-T joint sees it). **Left context shrinks 70 → 56 frames** in the chunked_limited mask (training-time `att_context_size=[56,13]`); the runtime mask code reads the GGUF KV, so this is a data-only delta. Vocab grows 1024 → 13087 with 39 explicit `` SPM tag tokens used by `target_lang=auto`. Auxiliary CTC head present in the upstream checkpoint but dropped at convert time per family Open-Decision #1. +- **`parakeet-ultra`** (moondream, reference **kestrel 0.9.1**, not NeMo): v3 geometry and weights layout (24 layers, d_model=1024, n_mels=128, `use_bias=false`, TDT head, pred_n_layers=2) plus three kestrel-runtime deltas, all gated on GGUF KVs so no other variant changes: + - **Length masking** (`stt.parakeet.encoder.length_masking="kestrel"`; ref `kestrel/models/parakeet_tdt/model.py` `Subsampling.forward`, `Encoder.forward_subsampled`, `conformer_ops.depthwise_conv_bn_silu`/`rel_attention`): valid mel frames = `n_samples // hop`, `(L-1)//2+1` per strided conv; the subsampler zeroes the padded time tail right after each strided conv (before the pointwise conv + ReLU) at every batch size; attention keys and conv rows past the valid length are masked and decode stops there. `ConvPolicy::pre_encode_mask_after_stride`; batched graphs add per-utterance extent masks after relu3/relu6 so a padded batch reproduces single runs. Gate: `enc.pre_encode.out`, `seg..enc.final`. + - **Pre_encode pointwise convs in F32** (`ConvPolicy::pre_encode_f32_pointwise`, `conv_2d_f32`): vendored `ggml_conv_2d` im2cols to F16 (3.3e-4 rel at `enc.pre_encode.out`). Enabled for ultra only; the other variants still run the F16 im2col (follow-up). + - **TDT global symbol budget** (`stt.parakeet.tdt.symbol_budget="global"`; ref `ParakeetTdt.generate_encoded`): one budget of `max_symbols * T_valid` steps, blank+duration-0 advances one frame, no per-frame cap. + - **VAD head + long-form segmenter** (`vad.{proj,ctx,out}`, `stt.parakeet.vad.*`, `stt.parakeet.segmenter.*`; ref `vad.py`, `segment.py`, `asr/audio.py` `AudioChunks.chunks`): `build_vad_graph` (subsampler -> proj 1x1 + SiLU -> ctx k=5 + SiLU -> out 1x1 -> sigmoid) per 120 s scan block with per-block mel normalization; host `longform::pause_segments` (float64, ties-to-even round); `run_longform` decodes each segment like a short clip and stitches (space-joined text, timestamps shifted by segment start and clamped to its end, one result segment per decoded segment). Gate: `vad.b.{mel,pre_encode,proj,ctx,prob}`, `vad.prob`, `longform.segments` (exact), `seg..enc.{mel.in,final}`, stitched transcript exact, word timestamps. + - **Correctness regime**: `TRANSCRIBE_NO_FLASH=1` via manifest `cpp_env` (flash casts the rel-pos score bias to F16). diff --git a/reports/porting/parakeet/parakeet-ultra/intake.json b/reports/porting/parakeet/parakeet-ultra/intake.json new file mode 100644 index 000000000..947f9b635 --- /dev/null +++ b/reports/porting/parakeet/parakeet-ultra/intake.json @@ -0,0 +1,239 @@ +{ + "schema_version": "transcribe-intake-v1", + "family": "parakeet", + "hf_repo": "moondream/parakeet-ultra", + "hf_revision": "73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b", + "sources": { + "config": { + "kind": "hf_file", + "path": "config.json", + "status": "found" + }, + "preprocessor": { + "kind": "reference_code", + "path": "transformers ParakeetFeatureExtractor defaults; kestrel/models/parakeet_tdt/features.py", + "status": "accepted_gap", + "detail": "No preprocessor_config.json in the repo. kestrel's parakeet_features is the NeMo frontend (n_fft=512, hop=160, win=400, 128 mel, preemph 0.97, per-feature norm, log guard 2^-24); identical to v3, so carried from the v3 intake." + }, + "tokenizer_config": { + "kind": "hf_file", + "path": "tokenizer_config.json", + "status": "missing" + }, + "tokenizer_json": { + "kind": "hf_file", + "path": "tokenizer.json", + "status": "found" + }, + "generation_config": { + "kind": "hf_file", + "path": "generation_config.json", + "status": "missing" + }, + "safetensors_metadata": { + "kind": "hf_api", + "path": "HfApi.get_safetensors_metadata", + "status": "found", + "detail": "header-only floating dtype distribution; no tensor payloads downloaded" + }, + "reference_modeling_code": { + "kind": "reference_code", + "path": "kestrel==0.9.1 (PyPI) kestrel/models/parakeet_tdt/{model,features,vad,segment,longform,runtime,decode_graph}.py + kestrel-kernels==0.7.4", + "status": "found", + "detail": "Publisher's runtime (Photon). Readable Python modeling, VAD head and pause segmenter; fused ops come from kestrel_kernels, which ships macOS arm64 wheels with a CPUKernelRuntime (smoke-tested 2026-10-04: get_runtime(cpu) and (mps) both load). transformers ParakeetForTDT is not used as an oracle." + }, + "model_card": { + "kind": "hf_api", + "path": "https://huggingface.co/moondream/parakeet-ultra", + "status": "found", + "detail": "Languages, benchmark tables, long-form segmentation description; .eval_results/open_asr_leaderboard.yaml" + } + }, + "variants": [ + { + "name": "parakeet-ultra", + "memory_gb": 1.5, + "files": [] + } + ], + "config": { + "architecture_candidates": [ + "encoder-transducer" + ], + "key_fields": { + "architectures": [ + "ParakeetForTDT" + ], + "hidden_act": "relu", + "model_type": "parakeet_tdt", + "vocab_size": 8193, + "encoder.num_hidden_layers": 24, + "encoder.hidden_size": 1024, + "encoder.num_attention_heads": 8, + "encoder.conv_kernel_size": 9, + "encoder.subsampling_factor": 8, + "encoder.subsampling_conv_channels": 256, + "encoder.num_mel_bins": 128, + "encoder.scale_input": false, + "decoder_hidden_size": 640, + "num_decoder_layers": 2, + "durations": [ + 0, + 1, + 2, + 3, + 4 + ], + "blank_token_id": 8192, + "vad_head": "proj Conv1d(1024->128,k1)+SiLU, ctx Conv1d(128->128,k5,pad2)+SiLU, out Conv1d(128->1,k1), sigmoid; input = subsampler output" + } + }, + "dtype": { + "expected": "float32", + "source": "config", + "evidence": "config.dtype=float32 and both references (transformers ParakeetForTDT, kestrel load_model dtype=float32) compute in F32. Storage is mixed: every matmul/conv weight and the whole vad_head are F16 at rest (356 tensors), norms/biases/BN stats F32 (349). So the F32 GGUF is the parity artifact and an F16 GGUF is lossless at rest (F16 storage is the precision ceiling; the card's 'full precision' is not literal).", + "details": { + "config_declared": "float32", + "header_distribution": { + "I64": 24, + "F32": 349, + "F16": 356 + } + }, + "expected_f32_tensors": [] + }, + "frontend": { + "sample_rate": 16000, + "n_mels": 128, + "hop_length": 160, + "fft_size": 512, + "window": "hann_symmetric", + "normalization": "per_feature", + "preemphasis": 0.97, + "dither": 0.0, + "center": true, + "padding_mode": "constant", + "mel_filterbank_norm": "slaney" + }, + "tokenizer": { + "type": "sentencepiece", + "vocab_size": 8192, + "special_tokens": { + "pad": 2, + "blank": 8192, + "decoder_start": 8192 + }, + "has_language_tokens": false, + "vocab_sha256": "7c21c04ff6ff2f806ca43a8420479091479c805d2b7952825ae2acc4fec9424d" + }, + "capabilities": { + "languages": [ + "en", + "de", + "fr", + "es", + "it", + "pt", + "ru", + "uk", + "hr", + "sl", + "lv", + "lt", + "et", + "fi", + "sv", + "da", + "nl", + "pl", + "cs", + "sk", + "hu", + "ro", + "bg", + "el", + "mt" + ], + "language_detection": true, + "translation": false, + "timestamps": [ + "token", + "word", + "segment" + ], + "streaming": false, + "speaker_diarization": false + }, + "upstream_benchmarks": [ + { + "dataset": "LibriSpeech test-clean", + "language": "en", + "metric": "wer", + "score": 1.41, + "score_unit": "percent", + "source": "https://huggingface.co/moondream/parakeet-ultra", + "notes": "Photon on NVIDIA GPU, Open ASR Leaderboard normalizer. v3 = 1.52 on same pipeline." + }, + { + "dataset": "LibriSpeech test-other", + "language": "en", + "metric": "wer", + "score": 2.98, + "score_unit": "percent", + "source": "https://huggingface.co/moondream/parakeet-ultra", + "notes": "v3 = 3.13" + }, + { + "dataset": "Open ASR Leaderboard, 7 English sets (average)", + "language": "en", + "metric": "wer", + "score": 5.8, + "score_unit": "percent", + "source": "https://huggingface.co/moondream/parakeet-ultra", + "notes": "v3 = 6.26" + }, + { + "dataset": "FLEURS test, 25 languages (average)", + "language": null, + "metric": "wer", + "score": 9.55, + "score_unit": "percent", + "source": "https://huggingface.co/moondream/parakeet-ultra", + "notes": "v3 = 11.62; per-language table on card" + }, + { + "dataset": "TED-LIUM 3, 11 full talks (long-form)", + "language": "en", + "metric": "wer", + "score": 1.94, + "score_unit": "percent", + "source": "https://huggingface.co/moondream/parakeet-ultra", + "notes": "Uses Photon's VAD-head segmenter (<=30 s pause-cut segments). v3 = 2.71 via NeMo's long-audio path. Not comparable to a single-pass full-attention run." + } + ], + "reference_framework": "author_repo_kestrel", + "reference_rationale": "The publisher's runtime is the source of truth: every number on the card is Photon, which runs kestrel. kestrel owns the full pipeline we must reproduce: NeMo-style frontend (features.py), FastConformer + TDT greedy decode (model.py via kestrel_kernels CPUKernelRuntime), the vad_head forward (vad.py), and the long-form pause segmenter and segment stitching (segment.py, longform.py, runtime.py). It runs locally on CPU (kestrel 0.9.1 / kestrel-kernels 0.7.4, cp312 arm64). transformers ParakeetForTDT drops vad_head and is not used. NeMo cannot load this checkpoint (no .nemo).", + "architecture_pattern": "encoder-transducer", + "known_risks": [ + "Long-form recipe is in scope and must match kestrel exactly: audio >30 s is cut at VAD-head pauses into <=30 s segments (no overlap, no context margins), each segment decoded independently, texts joined with a single space, timestamps shifted by segment start. Our parakeet runtime today runs the whole clip through one full-attention pass (max_audio_ms=0); ultra needs a new segmented path, gated on the presence of vad_head tensors.", + "vad_head.* (proj Conv1d 1024->128 k1 + SiLU, ctx Conv1d 128->128 k5 pad2 + SiLU, out Conv1d 128->1 k1, sigmoid) runs on the subsampler output only (post-linear, pre rel-pos/xscale/conformer). Needs converter support, GGUF tensors and a subsampler-only graph. kestrel scans in 120 s blocks; this is valid because the subsampler is local, but block-edge frames must match kestrel's per-block feature normalization.", + "Segmenter is host-side logic in kestrel/segment.py: speech = sigmoid >= 0.5 at 80 ms frames, bridge gaps <0.1 s, drop runs <0.1 s, pauses >=0.2 s, cut at midpoint of the last pause fully inside (1 s, 30 s), else midpoint-in-window, else hard cut at 30 s; segments with no speech are skipped; VAD scanned in 120 s blocks (valid because the subsampler is local). Stage 4 must reproduce this exactly for long-form parity.", + "Checkpoint is HF-transformers naming (encoder.subsampling.layers.*, self_attn.q_proj/relative_k_proj, decoder.lstm, joint.head, encoder_projector); scripts/convert-parakeet.py only reads NeMo state dicts. Needs an HF->NeMo key remap (or a transformers-input path) plus fused-joint handling.", + "Weights are F16 at rest despite config.dtype=float32. F32 GGUF is a pure upcast used for parity; F16 GGUF loses nothing at rest and is the natural shipping 'full' file. Quant WER deltas should be read against the F16 file, and F16-vs-F32 GGUF differences reflect ggml F16 compute only.", + "Same TDT joint/duration-head and FastConformer rel-pos risks as v3; language_detection is implicit (no LID tokens), inherited from v3.", + "Photon's per-segment output joins segment texts with a single space and shifts timestamps by segment start; segment-boundary punctuation/casing may differ from a single-pass decode.", + "Per-feature normalization is computed per call in kestrel: the VAD scan normalizes over each 120 s block, while ASR normalizes over each segment. Our frontend must reproduce both scopes; normalizing over the whole file would shift VAD probabilities and segment features.", + "kestrel is the oracle but runs fused kestrel_kernels ops (CPUKernelRuntime). Tensor dumps need hooks around kestrel modules rather than a transformers-style output_hidden_states; check CPU vs CUDA kernel numerics before treating CPU dumps as ground truth for the card's GPU numbers.", + "Short-clip behavior: clips that fit in 30 s bypass VAD and segmentation entirely (fits_one_segment), so LibriSpeech/FLEURS never exercise the new path; a dedicated long-form set (card: TED-LIUM 3, 11 talks) is required for acceptance." + ], + "intake_gaps": [ + { + "field": "frontend", + "reason": "No preprocessor_config.json; carried from v3 and confirmed against kestrel features.py. Accepted." + }, + { + "field": "variants[].memory_gb", + "reason": "Estimated from F16 safetensors size (1.26 GB); not measured." + } + ] +} diff --git a/scripts/convert-parakeet.py b/scripts/convert-parakeet.py index 7e946eb2b..ff03f4ffa 100644 --- a/scripts/convert-parakeet.py +++ b/scripts/convert-parakeet.py @@ -59,6 +59,15 @@ stt.parakeet.tdt.{durations,max_symbols} stt.frontend.{type,num_mels,sample_rate,n_fft,win_length,hop_length, window,normalize,dither,pre_emphasis,f_min,f_max} + stt.parakeet.vad.{hidden,context_kernel,activation,speech_threshold, + min_speech_seconds,min_gap_seconds} (vad_head variants) + stt.parakeet.segmenter.{max_segment_seconds,min_segment_seconds, + min_pause_seconds,scan_block_seconds} (vad_head variants) + stt.parakeet.encoder.length_masking = "kestrel", + stt.parakeet.tdt.symbol_budget = "global" (kestrel_runtime variants) + +parakeet-ultra is the one HF-safetensors source (no .nemo); see +load_hf_safetensors_model(). """ from __future__ import annotations @@ -397,6 +406,29 @@ "license_name": "nvidia-open-model-license", "license_link": "https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/", }, + # Post-trained parakeet-tdt-0.6b-v3 plus a `vad_head`. HF safetensors + # only (F16 at rest, upcast to F32); see load_hf_safetensors_model(). + "parakeet-ultra": { + "variant": "tdt-0.6b-ultra", + "display_name": "Parakeet Ultra", + "version": "v1", + "size_label": "0.6B", + "head_kind": "tdt", + "expected_vocab_size": 8192, + # Same 25 languages as v3, in the moondream model card's order. + "languages": ["en", "de", "fr", "es", "it", "pt", "ru", "uk", "hr", "sl", "lv", "lt", "et", "fi", "sv", "da", "nl", "pl", "cs", "sk", "hu", "ro", "bg", "el", "mt"], + "lang_detect": True, + "source_format": "hf_safetensors", + "hf_revision": "73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b", + "has_vad_head": True, + # kestrel length masking + global TDT symbol budget (see the KV notes). + "kestrel_runtime": True, + "author": "Moondream", + "organization": "moondream", + "license": "cc-by-4.0", + "license_name": "Creative Commons Attribution 4.0", + "license_link": "https://creativecommons.org/licenses/by/4.0/", + }, } @@ -568,6 +600,227 @@ def load_nemo_model(model_spec: str, prefer_direct: bool = False): return _DirectNemoArchive(cfg, sd, sp_proto) +# --------------------------------------------------------------------------- +# HF-transformers safetensors loading +# --------------------------------------------------------------------------- +# A ParakeetForTDT checkpoint is mapped onto _DirectNemoArchive's surface +# (NeMo-shaped cfg, NeMo-named fp32 state_dict, GGUF tokenizer payload). + +# HF name -> NeMo name. Anything not matched is an error, so a new +# tensor in a future checkpoint fails loudly instead of being dropped. +_HF_TO_NEMO_RULES: list[tuple[re.Pattern, str]] = [ + (re.compile(r"^encoder\.subsampling\.layers\.(\d+)\.(weight|bias)$"), r"encoder.pre_encode.conv.\1.\2"), + (re.compile(r"^encoder\.subsampling\.linear\.(weight|bias)$"), r"encoder.pre_encode.out.\1"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.q_proj\.weight$"), r"encoder.layers.\1.self_attn.linear_q.weight"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.k_proj\.weight$"), r"encoder.layers.\1.self_attn.linear_k.weight"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.v_proj\.weight$"), r"encoder.layers.\1.self_attn.linear_v.weight"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.o_proj\.weight$"), r"encoder.layers.\1.self_attn.linear_out.weight"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.relative_k_proj\.weight$"), + r"encoder.layers.\1.self_attn.linear_pos.weight"), + (re.compile(r"^encoder\.layers\.(\d+)\.self_attn\.bias_([uv])$"), r"encoder.layers.\1.self_attn.pos_bias_\2"), + (re.compile(r"^encoder\.layers\.(\d+)\.conv\.norm\.(.+)$"), r"encoder.layers.\1.conv.batch_norm.\2"), + (re.compile(r"^encoder\.layers\.(\d+)\.((?:feed_forward[12]\.linear[12]|norm_feed_forward[12]|norm_self_att|" + r"norm_conv|norm_out|conv\.pointwise_conv[12]|conv\.depthwise_conv)\.(?:weight|bias))$"), + r"encoder.layers.\1.\2"), + (re.compile(r"^decoder\.embedding\.weight$"), r"decoder.prediction.embed.weight"), + (re.compile(r"^decoder\.lstm\.(.+)$"), r"decoder.prediction.dec_rnn.lstm.\1"), + (re.compile(r"^decoder\.decoder_projector\.(weight|bias)$"), r"joint.pred.\1"), + (re.compile(r"^encoder_projector\.(weight|bias)$"), r"joint.enc.\1"), + (re.compile(r"^joint\.head\.(weight|bias)$"), r"joint.joint_net.2.\1"), + (re.compile(r"^(vad_head\.(?:proj|ctx|out)\.(?:weight|bias))$"), r"\1"), +] + + +def _hf_to_nemo_key(key: str) -> str: + for pattern, repl in _HF_TO_NEMO_RULES: + if pattern.match(key): + return pattern.sub(repl, key) + raise KeyError(f"unmapped HF tensor name: {key!r}") + + +class _HfBpeAsSpm: + """The SentencePieceProcessor surface extract_tokenizer() reads, built + from an HF tokenizer.json that was exported from a SentencePiece BPE + model (NeMo's multilingual parakeet vocab). + + SPM BPE scores are 0 for the user-defined pieces at the front of the + vocab and -(rank) for merged pieces after them; tokenizer.json drops + scores but keeps both orders, so they are reconstructed exactly. Every + non-unk piece is NORMAL, as in that SPM model. + """ + + def __init__(self, tok: dict, blank_id: int): + model = tok["model"] + if model.get("type") != "BPE": + raise ValueError(f"expected a BPE tokenizer.json, got {model.get('type')!r}") + vocab = model["vocab"] + self._pieces = [None] * len(vocab) + for piece, idx in vocab.items(): + self._pieces[idx] = piece + if any(p is None for p in self._pieces): + raise ValueError("tokenizer.json vocab ids are not contiguous") + added = sorted(a["id"] for a in tok.get("added_tokens", []) if a["id"] != blank_id) + if added != list(range(len(added))): + raise ValueError("tokenizer.json added tokens are not a contiguous prefix of the vocab") + self._first_merged = len(added) + self._unk = vocab[model["unk_token"]] + + def vocab_size(self) -> int: + return len(self._pieces) + + def id_to_piece(self, i: int) -> str: + return self._pieces[i] + + def get_score(self, i: int) -> float: + return 0.0 if i < self._first_merged else -float(i - self._first_merged) + + def is_unknown(self, i: int) -> bool: + return i == self._unk + + def is_control(self, i: int) -> bool: + return False + + def is_unused(self, i: int) -> bool: + return False + + def is_byte(self, i: int) -> bool: + return False + + def unk_id(self) -> int: + return self._unk + + def bos_id(self) -> int: + return -1 + + def eos_id(self) -> int: + return -1 + + +class _HfSafetensorsModel: + """Same surface as _DirectNemoArchive, plus `.gguf_tokenizer` (the + extract_tokenizer() payload, since there is no SPM proto).""" + + def __init__(self, cfg: dict, state_dict, gguf_tokenizer: dict): + from omegaconf import OmegaConf + + self.cfg = OmegaConf.create(cfg) + self._sd = state_dict + self.gguf_tokenizer = gguf_tokenizer + self.joint = None # resolve_runtime_hparams reads cfg + state_dict + + def state_dict(self): + return self._sd + + def eval(self): + return self + + +def _nemo_cfg_from_hf_config(hf: dict) -> dict: + """NeMo-shaped cfg for read_hparams() from a ParakeetForTDT config.json. + Structural fields the C++ port hard-codes are asserted. The frontend is + the NeMo mel preprocessor kestrel reproduces; dither 0 (kestrel never dithers).""" + enc = hf["encoder_config"] + checks = { + "model_type": (hf.get("model_type"), "parakeet_tdt"), + "encoder.hidden_act": (enc.get("hidden_act"), "silu"), + "hidden_act (joint)": (hf.get("hidden_act"), "relu"), + "encoder.subsampling_conv_kernel_size": (enc.get("subsampling_conv_kernel_size"), 3), + "encoder.subsampling_conv_stride": (enc.get("subsampling_conv_stride"), 2), + "encoder.num_key_value_heads": (enc.get("num_key_value_heads"), enc.get("num_attention_heads")), + "encoder.attention_bias": (enc.get("attention_bias"), False), + "encoder.convolution_bias": (enc.get("convolution_bias"), False), + } + bad = {k: v for k, v in checks.items() if v[0] != v[1]} + if bad: + raise ValueError(f"config.json departs from the parakeet TDT shape the port supports: {bad}") + if enc["intermediate_size"] % enc["hidden_size"]: + raise ValueError("encoder intermediate_size is not a multiple of hidden_size") + durations = list(hf["durations"]) + return { + "encoder": { + "n_layers": enc["num_hidden_layers"], + "d_model": enc["hidden_size"], + "n_heads": enc["num_attention_heads"], + "ff_expansion_factor": enc["intermediate_size"] // enc["hidden_size"], + "conv_kernel_size": enc["conv_kernel_size"], + "subsampling_factor": enc["subsampling_factor"], + "subsampling_conv_channels": enc["subsampling_conv_channels"], + "pos_emb_max_len": enc["max_position_embeddings"], + "xscaling": bool(enc["scale_input"]), + "att_context_size": [-1, -1], + "att_context_style": "regular", + "conv_norm_type": "batch_norm", + "conv_context_size": None, + }, + "preprocessor": { + "_target_": "nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor", + "sample_rate": 16000, + "window_size": 0.025, + "window_stride": 0.01, + "features": enc["num_mel_bins"], + "n_fft": 512, + "window": "hann", + "normalize": "per_feature", + "dither": 0.0, + }, + "decoder": { + "vocab_size": hf["vocab_size"] - 1, + "prednet": {"pred_hidden": hf["decoder_hidden_size"], "pred_rnn_layers": hf["num_decoder_layers"]}, + }, + "joint": { + "num_extra_outputs": len(durations), + "jointnet": {"joint_hidden": hf["decoder_hidden_size"], "activation": hf["hidden_act"]}, + }, + "decoding": {"durations": durations, "greedy": {"max_symbols": hf["max_symbols_per_step"]}}, + } + + +def load_hf_safetensors_model(model_spec: str, revision: str | None) -> _HfSafetensorsModel: + """Load an HF ParakeetForTDT checkpoint (repo id or local snapshot dir) + into NeMo naming. F16 tensors are upcast to F32, which is exact.""" + import json + import torch + from safetensors.torch import load_file + + local = Path(model_spec).expanduser() + if local.is_dir(): + root = local + else: + from huggingface_hub import snapshot_download + + root = Path(snapshot_download( + model_spec, revision=revision, + allow_patterns=["config.json", "model.safetensors", "tokenizer.json"], + )) + print(f"Loading Parakeet from HF safetensors: {root}") + hf_cfg = json.loads((root / "config.json").read_text()) + cfg = _nemo_cfg_from_hf_config(hf_cfg) + + raw = load_file(str(root / "model.safetensors"), device="cpu") + sd = {} + n_upcast = 0 + for key, tensor in raw.items(): + name = _hf_to_nemo_key(key) + if tensor.is_floating_point(): + if tensor.dtype == torch.float16: + n_upcast += 1 + elif tensor.dtype != torch.float32: + raise ValueError(f"{key}: unsupported source dtype {tensor.dtype}") + tensor = tensor.to(torch.float32) + sd[name] = tensor + print(f" {len(sd)} tensors, {n_upcast} upcast F16 -> F32 (lossless)") + + blank_id = int(hf_cfg["blank_token_id"]) + tok_json = json.loads((root / "tokenizer.json").read_text()) + blank_piece = next((a["content"] for a in tok_json.get("added_tokens", []) if a["id"] == blank_id), None) + if blank_piece != "": + raise ValueError(f"tokenizer.json id {blank_id} is {blank_piece!r}, expected ''") + sp = _HfBpeAsSpm(tok_json, blank_id) + if sp.vocab_size() != blank_id: + raise ValueError(f"blank_token_id {blank_id} != BPE vocab size {sp.vocab_size()}") + return _HfSafetensorsModel(cfg, sd, extract_tokenizer(sp)) + + # --------------------------------------------------------------------------- # Tokenizer extraction # --------------------------------------------------------------------------- @@ -1215,6 +1468,19 @@ def _from_module_or_cfg(attr: str, cfg_key: str, default=None): ] +# Speech head on the subsampler output: +# proj Conv1d(d_model->H, k=1) + SiLU, ctx Conv1d(H->H, k, pad k//2) + SiLU, +# out Conv1d(H->1, k=1), sigmoid. F32 in every preset (see policy.cpp). +VAD_HEAD_TABLE: list[tuple[str, str]] = [ + ("vad_head.proj.weight", "vad.proj.weight"), + ("vad_head.proj.bias", "vad.proj.bias"), + ("vad_head.ctx.weight", "vad.ctx.weight"), + ("vad_head.ctx.bias", "vad.ctx.bias"), + ("vad_head.out.weight", "vad.out.weight"), + ("vad_head.out.bias", "vad.out.bias"), +] + + # Speaker kernels are top-level NeMo Sequential modules. Only the two # Linear slots (.0 and .3) carry tensors. def spk_kernel_table(layer: int) -> list[tuple[str, str]]: @@ -1294,7 +1560,8 @@ def tensor_to_fp32_numpy(t) -> np.ndarray: # --------------------------------------------------------------------------- -def convert(model_spec: str, out_path: Path, repo_id: str | None = None) -> None: +def convert(model_spec: str, out_path: Path, repo_id: str | None = None, + revision: str | None = None) -> None: from omegaconf import OmegaConf print(f"Output dtype: {REFERENCE_DTYPE_LABEL} (source/reference dtype)") @@ -1322,7 +1589,11 @@ def convert(model_spec: str, out_path: Path, repo_id: str | None = None) -> None f"{', prompt=on' if has_prompt else ''}" f"{', spk_kernels=on' if has_spk_kernels else ''})") - model = load_nemo_model(model_spec, prefer_direct=prefer_direct) + has_vad_head = bool(profile.get("has_vad_head", False)) + if profile.get("source_format") == "hf_safetensors": + model = load_hf_safetensors_model(model_spec, revision or profile.get("hf_revision")) + else: + model = load_nemo_model(model_spec, prefer_direct=prefer_direct) config = OmegaConf.to_container(model.cfg, resolve=True) hp = read_hparams(config) @@ -1408,11 +1679,13 @@ def convert(model_spec: str, out_path: Path, repo_id: str | None = None) -> None # SPM instance NeMo hands out through model.tokenizer.tokenizer has # vocab_size monkey-patched to a property-like int on some builds, # which breaks the extract_tokenizer() contract. - import sentencepiece as spm - proto = model.tokenizer.tokenizer.serialized_model_proto() - sp = spm.SentencePieceProcessor() - sp.LoadFromSerializedProto(proto) - tok = extract_tokenizer(sp) + tok = getattr(model, "gguf_tokenizer", None) + if tok is None: + import sentencepiece as spm + proto = model.tokenizer.tokenizer.serialized_model_proto() + sp = spm.SentencePieceProcessor() + sp.LoadFromSerializedProto(proto) + tok = extract_tokenizer(sp) if len(tok["tokens"]) != hp["pred_vocab"]: raise ValueError( f"tokenizer length mismatch: {len(tok['tokens'])} tokens " @@ -1685,6 +1958,36 @@ def convert(model_spec: str, out_path: Path, repo_id: str | None = None) -> None writer.add_float32("stt.frontend.f_min", hp["fe_f_min"]) writer.add_float32("stt.frontend.f_max", hp["fe_f_max"]) + # kestrel-runtime semantics. Absent on every NeMo-validated variant. + if profile.get("kestrel_runtime"): + writer.add_string("stt.parakeet.encoder.length_masking", "kestrel") + writer.add_string("stt.parakeet.tdt.symbol_budget", "global") + + # VAD head shape from the checkpoint; segmenter constants are kestrel's + # runtime policy, written here so the C++ does not hard-code a copy. + if has_vad_head: + vad_proj = sd["vad_head.proj.weight"] + vad_ctx = sd["vad_head.ctx.weight"] + vad_out = sd["vad_head.out.weight"] + vad_hidden = int(vad_proj.shape[0]) + if (tuple(vad_proj.shape) != (vad_hidden, hp["enc_d_model"], 1) + or tuple(vad_ctx.shape[:2]) != (vad_hidden, vad_hidden) + or vad_ctx.shape[2] % 2 != 1 + or tuple(vad_out.shape) != (1, vad_hidden, 1)): + raise ValueError( + f"unexpected vad_head shapes: proj {tuple(vad_proj.shape)}, " + f"ctx {tuple(vad_ctx.shape)}, out {tuple(vad_out.shape)}") + writer.add_uint32 ("stt.parakeet.vad.hidden", vad_hidden) + writer.add_uint32 ("stt.parakeet.vad.context_kernel", int(vad_ctx.shape[2])) + writer.add_string ("stt.parakeet.vad.activation", "silu") + writer.add_float32("stt.parakeet.vad.speech_threshold", 0.5) + writer.add_float32("stt.parakeet.vad.min_speech_seconds", 0.1) + writer.add_float32("stt.parakeet.vad.min_gap_seconds", 0.1) + writer.add_float32("stt.parakeet.segmenter.max_segment_seconds", 30.0) + writer.add_float32("stt.parakeet.segmenter.min_segment_seconds", 1.0) + writer.add_float32("stt.parakeet.segmenter.min_pause_seconds", 0.2) + writer.add_float32("stt.parakeet.segmenter.scan_block_seconds", 120.0) + # ----- tensors ----- consumed: set[str] = set() n_added = 0 @@ -1776,6 +2079,11 @@ def add_combined(nemo_a: str, nemo_b: str, gguf_name: str) -> None: for nemo_name, gguf_name in JOINT_TABLE: add(nemo_name, gguf_name) + # VAD head: three Conv1d layers, PyTorch [out, in, k] layout. + if has_vad_head: + for nemo_name, gguf_name in VAD_HEAD_TABLE: + add(nemo_name, gguf_name) + # Prompt MLP. Shapes were already validated above when the prompt # KVs were emitted; here we just copy the tensors verbatim. if has_prompt: @@ -1814,12 +2122,14 @@ def add_combined(nemo_a: str, nemo_b: str, gguf_name: str) -> None: spk_tensors = len(spk_kernel_layers) * per_spk_layer else: spk_tensors = 0 + vad_tensors = len(VAD_HEAD_TABLE) if has_vad_head else 0 expected = ( len(PRE_ENCODE_TABLE) + hp["enc_n_layers"] * per_layer_tensors + head_tensors + prompt_tensors + spk_tensors + + vad_tensors ) if n_added != expected: raise RuntimeError( @@ -1858,7 +2168,8 @@ def main(argv: list[str]) -> int: p.add_argument( "model", type=str, - help="HF repo id (e.g. nvidia/parakeet-tdt-0.6b-v2) or local .nemo path", + help="HF repo id (e.g. nvidia/parakeet-tdt-0.6b-v2), local .nemo path, " + "or local HF snapshot dir (HF-safetensors variants)", ) p.add_argument( "out_path", @@ -1874,6 +2185,13 @@ def main(argv: list[str]) -> int: help="HF repo id used to derive the output slug when converting " "from a local path. Ignored if out_path is given.", ) + p.add_argument( + "--revision", + type=str, + default=None, + help="HF revision for HF-safetensors variants (parakeet-ultra); " + "defaults to the profile's pinned revision.", + ) args = p.parse_args(argv[1:]) out_path = args.out_path @@ -1894,7 +2212,7 @@ def main(argv: list[str]) -> int: repo_id = args.repo_id if repo_id is None and "/" in args.model and not Path(args.model).exists(): repo_id = args.model - convert(args.model, out_path, repo_id=repo_id) + convert(args.model, out_path, repo_id=repo_id, revision=args.revision) return 0 diff --git a/scripts/dump_reference_parakeet_kestrel.py b/scripts/dump_reference_parakeet_kestrel.py new file mode 100644 index 000000000..e2b1cfc80 --- /dev/null +++ b/scripts/dump_reference_parakeet_kestrel.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +"""Reference dumps for parakeet-ultra from kestrel (the publisher's runtime). + +Tensor names match scripts/dump_reference_parakeet_nemo.py wherever the +quantity is the same, so the C++ parakeet observer points compare as-is: + + encoder enc.mel.in [n_mels, T] normalized log-mel, frames on the last axis + enc.pre_encode.out [T', d] subsampler output (pre rel-pos) + enc.pos_emb [2T'-1, d] sinusoid table, positions T'-1 .. -(T'-1) + enc.block..out [T', d] block output (default blocks 0, 12, 23) + enc.block..{ff1,attn,conv,ff2} running residual (default block 0) + enc.final [T', d] encoder output before encoder_projector + decode dec.enc_out [T', d] == enc.final + dec.embed.0 [d_pred] predictor input at step 0 (blank row, zeros) + dec.lstm..{h,c}.0 [d_pred] LSTM state after the blank step + dec.joint.0 [V+5] log_softmax over the whole joint row for + (frame 0, start state); NeMo/C++ convention. + kestrel argmaxes raw logits per slice, which + this shift does not change. + transcript.json kestrel runtime text (+ token ids) + longform vad.b.mel [n_mels, Tb] features of VAD scan block k (120 s blocks, + each normalized on its own) + vad.b.pre_encode [Tb', d] subsampler output for the block + vad.b.proj [Tb', 128] SiLU(proj) + vad.b.ctx [Tb', 128] SiLU(ctx) + vad.b.prob [Tb'] sigmoid(out), valid frames only + vad.prob [N] all blocks' valid-frame probabilities, in order + seg..enc.mel.in [n_mels, T] features of decoded segment j + seg..enc.final [T'v, d] encoder output for segment j, valid rows only + longform.segments [n, 2] (start sample, sample count) per segment, exact + segments.json blocks, speech regions, cut points (sample + indices), per-segment tokens/durations/text + transcript.json kestrel runtime text + word-timestamped segments + +Usage: + uv run --project scripts/envs/parakeet-kestrel \\ + scripts/dump_reference_parakeet_kestrel.py {encoder,decode,longform} \\ + --model moondream/parakeet-ultra --revision \\ + --audio samples/jfk.wav --out build/validate/parakeet/parakeet-ultra/jfk/ref +""" + +from __future__ import annotations + +import argparse +import json +import sys +import types +from functools import partial +from importlib.metadata import version +from pathlib import Path +from typing import Any + +import numpy as np + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from lib.ref_dump import write_tensor, write_transcript # noqa: E402 + +SAMPLE_RATE = 16000 + + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- + +def configure_torch(args: argparse.Namespace) -> None: + import torch + + torch.set_num_threads(max(1, int(args.torch_threads))) + torch.manual_seed(0) + + +def load(args: argparse.Namespace): + import torch + from kestrel.models.parakeet_tdt.weights import load_parakeet_tdt + + loaded = load_parakeet_tdt( + args.model, revision=args.revision, device="cpu", dtype=torch.float32 + ) + if loaded.model.vad_head is None: + raise SystemExit(f"error: {args.model} carries no vad_head; this dumper is for parakeet-ultra") + return loaded.model, loaded.tokenizer + + +def make_runtime(model, tokenizer, args: argparse.Namespace): + import torch + from kestrel.models.parakeet_tdt.runtime import ParakeetTdtRuntime + + cfg = types.SimpleNamespace( + model=args.model, + device="cpu", + dtype=torch.float32, + cpu_threads=max(1, int(args.torch_threads)), + enable_cuda_graphs=False, + ) + return ParakeetTdtRuntime(cfg, model=model, tokenizer=tokenizer) + + +def load_audio(path: Path) -> np.ndarray: + import soundfile as sf + + pcm, sr = sf.read(str(path), dtype="float32", always_2d=False) + if sr != SAMPLE_RATE: + raise SystemExit(f"error: {path} is {sr} Hz, expected {SAMPLE_RATE}") + if pcm.ndim != 1: + raise SystemExit(f"error: {path} is not mono") + return pcm + + +def make_source(args: argparse.Namespace, audio: Path, n_samples: int) -> dict[str, Any]: + return { + "framework": "kestrel", + "kestrel_version": version("kestrel"), + "model": args.model, + "revision": args.revision, + "audio": audio.name, + "n_samples": n_samples, + "sample_rate": SAMPLE_RATE, + "device": "cpu", + "dtype": "float32", + "language_hint_ignored": args.language, + } + + +def to_np(t) -> np.ndarray: + return t.detach().float().cpu().numpy().astype(np.float32) + + +def features_for(waveform: np.ndarray): + """The runtime's own feature path for one row (parakeet_cohort_features).""" + import torch + from kestrel.models.parakeet_tdt.features import parakeet_cohort_features + + staged = torch.from_numpy(np.ascontiguousarray(waveform, dtype=np.float32))[None] + return parakeet_cohort_features(staged, [int(waveform.size)]) + + +# --------------------------------------------------------------------------- +# Encoder capture (re-executes Encoder.forward_subsampled with taps) +# --------------------------------------------------------------------------- + +def run_encoder(model, features, mask, blocks: list[int], sub_blocks: list[int]) -> dict[str, Any]: + """Same ops, same order as kestrel's Encoder.forward / EncoderBlock.forward; + asserts bitwise equality with the un-tapped forward at the end.""" + import torch + from kestrel_kernels import get_runtime + + from kestrel.models.parakeet_tdt.model import _norm_args + + enc = model.encoder + taps: dict[str, Any] = {} + with torch.inference_mode(): + hidden, valid = enc.subsampling(features, mask) + taps["enc.pre_encode.out"] = hidden[0] + length = hidden.shape[1] + relative_positions = torch.arange(length - 1, -length, -1, device=hidden.device) + phase = torch.outer(relative_positions.float(), enc.inverse_frequency) + positions = torch.stack((phase.sin(), phase.cos()), dim=-1).flatten(-2) + positions = positions[None].expand(hidden.shape[0], -1, -1).to(hidden.dtype) + taps["enc.pos_emb"] = positions[0] + relative = enc.relative_k_proj(positions) + relative = relative.view(*relative.shape[:2], len(enc.layers), -1) + relative = relative.permute(2, 0, 1, 3).contiguous() + pair_mask = valid[:, :, None] & valid[:, None, :] + for index, (layer, rel_k) in enumerate(zip(enc.layers, relative)): + if index in sub_blocks: + conformer = get_runtime(hidden.device).conformer + normed = conformer.layer_norm(hidden, *_norm_args(layer.norm_feed_forward1)) + h, normed = conformer.add_scaled_layer_norm( + hidden, layer.feed_forward1(normed), 0.5, *_norm_args(layer.norm_self_att)) + taps[f"enc.block.{index}.ff1"] = h[0] + h, normed = conformer.add_scaled_layer_norm( + h, layer.self_attn(normed, rel_k, pair_mask), 1.0, *_norm_args(layer.norm_conv)) + taps[f"enc.block.{index}.attn"] = h[0] + h, normed = conformer.add_scaled_layer_norm( + h, layer.conv(normed, valid), 1.0, *_norm_args(layer.norm_feed_forward2)) + taps[f"enc.block.{index}.conv"] = h[0] + h, out = conformer.add_scaled_layer_norm( + h, layer.feed_forward2(normed), 0.5, *_norm_args(layer.norm_out)) + taps[f"enc.block.{index}.ff2"] = h[0] + hidden = out + else: + hidden = layer(hidden, rel_k, pair_mask, valid) + if index in blocks: + taps[f"enc.block.{index}.out"] = hidden[0] + taps["enc.final"] = hidden[0] + reference, _ = enc(features, mask) + if not torch.equal(reference[0], hidden[0]): + diff = (reference[0] - hidden[0]).abs().max().item() + raise SystemExit(f"error: tapped encoder diverged from kestrel Encoder.forward (max_abs={diff:.3e})") + taps["_valid"] = valid + return taps + + +def default_blocks(model, requested: list[int] | None) -> list[int]: + n = len(model.encoder.layers) + if requested: + return sorted({i for i in requested if 0 <= i < n}) + return sorted({0, n // 2, n - 1}) + + +# --------------------------------------------------------------------------- +# Subcommands +# --------------------------------------------------------------------------- + +def cmd_encoder(args: argparse.Namespace) -> int: + configure_torch(args) + model, _tok = load(args) + audio = Path(args.audio).resolve() + out_dir = Path(args.out).resolve() + pcm = load_audio(audio) + source = make_source(args, audio, pcm.size) + + def dump(name: str, t, stage: str) -> None: + a = to_np(t) + print(f" {name}: shape={a.shape} min={a.min():.4e} max={a.max():.4e}") + write_tensor(name, a, stage, {**source, "hook": name}, out_dir=out_dir) + + features, mask = features_for(pcm) + blocks = default_blocks(model, args.blocks) + sub_blocks = sorted(set(args.sub_blocks if args.sub_blocks is not None else [0])) + taps = run_encoder(model, features, mask, blocks, sub_blocks) + + dump("enc.mel.in", features[0].transpose(0, 1), "frontend.mel.norm") + dump("enc.pre_encode.out", taps["enc.pre_encode.out"], "encoder.pre_encode") + dump("enc.pos_emb", taps["enc.pos_emb"], "encoder.pos_emb") + for i in sorted(set(blocks) | set(sub_blocks)): + for tag in ("ff1", "attn", "conv", "ff2", "out"): + key = f"enc.block.{i}.{tag}" + if key in taps: + dump(key, taps[key], f"encoder.block{i}.{tag}") + dump("enc.final", taps["enc.final"], "encoder.final") + return 0 + + +def cmd_decode(args: argparse.Namespace) -> int: + import torch + + configure_torch(args) + model, tokenizer = load(args) + audio = Path(args.audio).resolve() + out_dir = Path(args.out).resolve() + pcm = load_audio(audio) + source = make_source(args, audio, pcm.size) + if pcm.size > 30 * SAMPLE_RATE: + raise SystemExit("error: decode dumps one whole-clip pass; clips over 30 s go through `longform`") + + def dump(name: str, t, stage: str) -> None: + a = to_np(t) + print(f" {name}: shape={a.shape} min={a.min():.4e} max={a.max():.4e}") + write_tensor(name, a, stage, {**source, "hook": name}, out_dir=out_dir) + + features, mask = features_for(pcm) + cfg = model.config + with torch.inference_mode(): + encoded, valid = model.encoder(features, mask) + dump("dec.enc_out", encoded[0], "decoder.enc_out") + token = torch.tensor([[cfg.blank_token_id]]) + dump("dec.embed.0", model.decoder.embedding(token)[0, 0], "decoder.embed") + decoder_hidden, (h, c) = model.decoder(token, None) + for layer in range(h.shape[0]): + dump(f"dec.lstm.{layer}.h.0", h[layer, 0], "decoder.lstm") + dump(f"dec.lstm.{layer}.c.0", c[layer, 0], "decoder.lstm") + projected = model.encoder_projector(encoded) + logits = model.joint(projected[:, 0:1], decoder_hidden) + dump("dec.joint.0", torch.log_softmax(logits.float(), dim=-1).reshape(-1), "decoder.joint") + out = model.generate(features, mask) + + seq = out.sequences[0, : int(out.lengths[0])].tolist() + tokens = [t for t in seq if t not in (tokenizer.blank_token_id, tokenizer.pad_token_id)] + + runtime = make_runtime(model, tokenizer, args) + result = runtime.forward("transcribe", [{"audio": pcm, "sample_rate": SAMPLE_RATE, "timestamps": "word"}])[0] + if isinstance(result, Exception): + raise result + text = result["text"] + if text != tokenizer.decode(seq).strip(): + raise SystemExit("error: runtime text differs from model.generate on a single segment") + print(f" transcript: {text}") + write_transcript(out_dir, text, source={**source, "segments": result["segments"]}, tokens=tokens) + return 0 + + +def cmd_longform(args: argparse.Namespace) -> int: + """Run kestrel's own segmenter (pause_segments + head_speech) and record + every VAD block, every cut, and every decoded segment.""" + import torch + import torch.nn.functional as F + from kestrel.models.asr.audio import AudioChunks + from kestrel.models.parakeet_tdt import segment as kseg + from kestrel.models.parakeet_tdt.vad import head_speech + + configure_torch(args) + model, tokenizer = load(args) + audio = Path(args.audio).resolve() + out_dir = Path(args.out).resolve() + pcm = load_audio(audio) + source = make_source(args, audio, pcm.size) + + def dump(name: str, t, stage: str) -> None: + a = to_np(t) + print(f" {name}: shape={a.shape} min={a.min():.4e} max={a.max():.4e}") + write_tensor(name, a, stage, {**source, "hook": name}, out_dir=out_dir) + + # Tap the head: kestrel's head_speech calls model.speech_probabilities once per + # scanned block. The tap re-runs the head's three convolutions and asserts the + # result equals the real call before recording. + blocks: list[dict[str, Any]] = [] + original = model.speech_probabilities + + def tapped(features, attention_mask): + probs, valid = original(features, attention_mask) + with torch.inference_mode(): + hidden, _ = model.encoder.subsampling(features, attention_mask) + head = model.vad_head + proj = F.silu(head.proj(hidden.transpose(1, 2))) + ctx = F.silu(head.ctx(proj)) + mine = torch.sigmoid(head.out(ctx).squeeze(1).float()) + if not torch.equal(mine, probs): + raise SystemExit("error: tapped vad_head diverged from speech_probabilities") + n = int(valid[0].sum()) + blocks.append({ + "mel": features[0].transpose(0, 1), + "pre_encode": hidden[0], + "proj": proj[0].transpose(0, 1), + "ctx": ctx[0].transpose(0, 1), + "prob": probs[0, :n], + "mel_frames": int(attention_mask[0].sum()), + "valid_frames": n, + }) + return probs, valid + + model.speech_probabilities = tapped + block_regions: list[list[tuple[float, float]]] = [] + block_samples: list[int] = [] + + def speech(waveform: np.ndarray, sample_rate: int): + regions = head_speech(model, waveform, sample_rate) + block_regions.append([list(r) for r in regions]) + block_samples.append(int(waveform.size)) + return regions + + segments: list[dict[str, Any]] = [] + with AudioChunks(pcm, sample_rate=SAMPLE_RATE, clip_start_seconds=0.0, clip_end_seconds=None, + target_sample_rate=SAMPLE_RATE, max_duration_seconds=24 * 60 * 60) as chunks: + for piece in kseg.pause_segments(chunks, speech): + start = int(round(piece.clip_start_seconds * SAMPLE_RATE)) + segments.append({ + "index": len(segments), + "start_sample": start, + "n_samples": int(piece.waveform.size), + "start_seconds": piece.clip_start_seconds, + "duration_seconds": piece.duration_seconds, + "_waveform": piece.waveform, + }) + model.speech_probabilities = original + + # Cut points as an exact-compare tensor: [n_segments, 2] = (start sample, + # sample count). Integer-valued f32 is exact below 2^24 samples (17 min). + if segments: + cuts = np.array([[sg["start_sample"], sg["n_samples"]] for sg in segments], dtype=np.float64) + if cuts.max() >= 2**24: + raise SystemExit("error: longform.segments needs samples < 2^24 to be exact in f32") + dump("longform.segments", torch.from_numpy(cuts.astype(np.float32)), "longform.segments") + + # The segmenter is contiguous: every segment is a slice of the file. + for seg in segments: + s, n = seg["start_sample"], seg["n_samples"] + if not np.array_equal(seg["_waveform"], pcm[s:s + n]): + raise SystemExit(f"error: segment {seg['index']} is not pcm[{s}:{s + n}]") + + for k, block in enumerate(blocks): + dump(f"vad.b{k}.mel", block["mel"], "vad.mel") + dump(f"vad.b{k}.pre_encode", block["pre_encode"], "vad.pre_encode") + dump(f"vad.b{k}.proj", block["proj"], "vad.proj") + dump(f"vad.b{k}.ctx", block["ctx"], "vad.ctx") + dump(f"vad.b{k}.prob", block["prob"], "vad.prob") + if blocks: + dump("vad.prob", torch.cat([b["prob"] for b in blocks]), "vad.prob") + + # Decode each segment the way the runtime does (one row, cohort features, + # model.generate) and record tokens/durations and the encoder output. + for seg in segments: + wav = seg.pop("_waveform") + features, mask = features_for(wav) + with torch.inference_mode(): + encoded, valid = model.encoder(features, mask) + out = model.generate(features, mask) + j = seg["index"] + n_valid = int(valid[0].sum()) + seg["valid_frames"] = n_valid + # C++ "seg.." names; valid rows only (padded rows are unspecified). + dump(f"seg.{j}.enc.mel.in", features[0].transpose(0, 1), "segment.mel") + dump(f"seg.{j}.enc.final", encoded[0, :n_valid], "segment.encoder.final") + n = int(out.lengths[0]) + seg["token_ids"] = out.sequences[0, :n].tolist() + seg["durations"] = out.durations[0, :n].tolist() + seg["text"] = tokenizer.decode(seg["token_ids"]).strip() + + runtime = make_runtime(model, tokenizer, args) + result = runtime.forward("transcribe", [{"audio": pcm, "sample_rate": SAMPLE_RATE, "timestamps": "word"}])[0] + if isinstance(result, Exception): + raise result + stitched = " ".join(s["text"] for s in segments if s["text"]) + if stitched != result["text"]: + raise SystemExit("error: per-segment decode does not reproduce the runtime transcript") + + meta = { + "constants": { + "segment_seconds": kseg.SEGMENT_SECONDS, + "min_pause_seconds": kseg.MIN_PAUSE_SECONDS, + "min_segment_seconds": kseg.MIN_SEGMENT_SECONDS, + "block_seconds": kseg.BLOCK_SECONDS, + "encoder_frame_seconds": model.encoder_frame_seconds, + }, + "n_samples": int(pcm.size), + "fits_one_segment": pcm.size <= kseg.SEGMENT_SECONDS * SAMPLE_RATE, + "vad_blocks": [ + {"index": k, "n_samples": block_samples[k], "mel_frames": b["mel_frames"], + "valid_frames": b["valid_frames"], "speech_regions_seconds": block_regions[k]} + for k, b in enumerate(blocks) + ], + "segments": segments, + "source": source, + } + (out_dir / "segments.json").write_text(json.dumps(meta, indent=2) + "\n") + print(f" {len(blocks)} VAD blocks, {len(segments)} segments: " + f"{[(s['start_sample'], s['n_samples']) for s in segments]}") + print(f" transcript: {result['text'][:200]}...") + tokens = [t for s in segments for t in s["token_ids"] + if t not in (tokenizer.blank_token_id, tokenizer.pad_token_id)] + write_transcript(out_dir, result["text"], source={**source, "segments": result["segments"]}, tokens=tokens) + return 0 + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def add_common_args(p: argparse.ArgumentParser) -> None: + p.add_argument("--model", required=True, help="HF repo id (moondream/parakeet-ultra) or local snapshot dir") + p.add_argument("--revision", default=None, help="HF revision to pin (kestrel defaults to main)") + p.add_argument("--audio", required=True, help="16 kHz mono wav") + p.add_argument("--out", required=True, help="Output directory") + p.add_argument("--torch-threads", type=int, default=1, help="Torch threads (default 1 for deterministic dumps)") + p.add_argument("--language", default=None, + help="Accepted for validate.py compatibility and ignored: kestrel rejects language forcing") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = parser.add_subparsers(dest="command", required=True) + ep = sub.add_parser("encoder", help="Whole-clip encoder intermediates (clips <= 30 s)") + add_common_args(ep) + ep.add_argument("--blocks", type=int, nargs="*", default=None, help="Block outputs to dump (default 0, mid, last)") + ep.add_argument("--sub-blocks", type=int, nargs="*", default=None, help="Blocks to dump ff1/attn/conv/ff2 for (default 0)") + ep.set_defaults(func=cmd_encoder) + dp = sub.add_parser("decode", help="Decoder first step + joint + runtime transcript (clips <= 30 s)") + add_common_args(dp) + dp.set_defaults(func=cmd_decode) + lp = sub.add_parser("longform", help="VAD-head scan, pause segmentation, per-segment decode, stitched transcript") + add_common_args(lp) + lp.set_defaults(func=cmd_longform) + args = parser.parse_args() + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/envs/parakeet-kestrel/pyproject.toml b/scripts/envs/parakeet-kestrel/pyproject.toml new file mode 100644 index 000000000..ea0cbc232 --- /dev/null +++ b/scripts/envs/parakeet-kestrel/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "transcribe-parakeet-kestrel-env" +version = "0.1.0" +description = "Python env for parakeet-ultra reference dumps via kestrel (Photon's engine)" +requires-python = ">=3.12,<3.13" +dependencies = [ + "kestrel==0.9.1", + "kestrel-kernels==0.7.4", + "kestrel-native==0.1.8", + "torch>=2.8", + "huggingface-hub>=0.20", + "soundfile>=0.12", + "numpy>=1.26", +] + +# Laptop dumps on darwin (CPUKernelRuntime), Modal sweeps on linux/cuda. +[tool.uv] +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", +] diff --git a/scripts/envs/parakeet-kestrel/uv.lock b/scripts/envs/parakeet-kestrel/uv.lock new file mode 100644 index 000000000..baea5f905 --- /dev/null +++ b/scripts/envs/parakeet-kestrel/uv.lock @@ -0,0 +1,803 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" +resolution-markers = [ + "platform_machine != 'aarch64' and sys_platform == 'darwin'", + "platform_machine == 'aarch64' and sys_platform == 'darwin'", + "platform_machine != 'aarch64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] +supported-markers = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/11/1293429c1c3a3e19b551275e65efddd122a905bbe7e368816a59f3ef2a41/cuda_bindings-13.4.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfbd3f7d4ac04dd41dc49121b9e408c8283992f47124c2290ecb79bbbadcca8e", size = 6488226, upload-time = "2026-09-23T02:22:00.578Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/efb6bbb7307bf5c83dc4acca650280b210c67ed1a1a60f898a90e7c82e38/cuda_bindings-13.4.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f72bcfcdf3be23e1da3c792f68f508586f48d037bca8b10f552c4cca5971f2", size = 7172185, upload-time = "2026-09-23T02:22:03.114Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fb/f8e1890428f9f590b4beebd63b068aac1ce32a3331510c847b9f9a78f261/cuda_pathfinder-1.8.3-py3-none-any.whl", hash = "sha256:e29e59829c297a7a5233bd9cc71094fc5bddbd076951482670178f9eade39b1f", size = 62561, upload-time = "2026-10-02T03:20:23.712Z" }, +] + +[[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')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +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')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "filelock" +version = "4.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/51/a182494d1d8dde1240bff84dda57d48165d982e59582ce8f167e8e3d7628/filelock-4.0.10.tar.gz", hash = "sha256:00d6a81f976a6332551c2c10f39e12b4abb7e01c64d4c497b81a615bc9186f1f", size = 252487, upload-time = "2026-10-03T23:40:48.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/2a/4b1ea3f2180be251a5cff1e70c8a5b7994ffa5c8eab8937901f8a9db58fe/filelock-4.0.10-py3-none-any.whl", hash = "sha256:698048d158dc3c9e18cc5e88b3efdd4bd0e77d76686b22adb4511cc4b2c2f0db", size = 111193, upload-time = "2026-10-03T23:40:47.334Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/cd/9be253869fc42e764de7f3dedd6969af7d44ff9c3375214a3442a6f3fc08/fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe", size = 333545, upload-time = "2026-09-18T17:50:42.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/c0/a98505f18594f1bce828bb159cec0fcf9860562f1a2c85913409fc8f3d9e/fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f", size = 221738, upload-time = "2026-09-18T17:50:41.341Z" }, +] + +[[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.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +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", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpcore", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +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.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "hf-xet", marker = "(platform_machine == 'AMD64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'darwin') or (platform_machine == 'amd64' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/2a/484d112c0d8fc5f665d7b65137ac9cdb2953c982391598c3597968a12ee7/huggingface_hub-1.33.0.tar.gz", hash = "sha256:367be21a201db9523eddf8aeac7048f2602c1b308691c97640d5e72ed188007e", size = 1043548, upload-time = "2026-09-24T09:49:29.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/16/963096d224b80909432dc16561a615fd33d2d13beef3ce4c63fa25e40867/huggingface_hub-1.33.0-py3-none-any.whl", hash = "sha256:04e434b06e100eddbce9a6e817d72693a7884b10a79bd67ab48080d5c07eb899", size = 846435, upload-time = "2026-09-24T09:49:28.059Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +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 = "kestrel" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "kestrel-kernels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "kestrel-native", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch-c-dlpack-ext", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/26/d6a3530617148deff50f92323425601b31006393d3b9f74e1cd3340a5792/kestrel-0.9.1.tar.gz", hash = "sha256:52083e88c71ccd6023096714033403d73a7cfb7a83b72493509bfb09163f344a", size = 565547, upload-time = "2026-09-30T00:39:50.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/ff/8f779728d192ba3a09d8e063193d180597372f223a2c1f9bcf37c26c21ac/kestrel-0.9.1-py3-none-any.whl", hash = "sha256:80640639279b18b2da61b08c06c550f8eaab6596d6d15978fd56f76707462cb7", size = 647466, upload-time = "2026-09-30T00:39:48.455Z" }, +] + +[[package]] +name = "kestrel-kernels" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kestrel-kernels-bundle-a", marker = "sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-b", marker = "sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-c", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-d", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-e", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-f", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-g", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-kernels-bundle-h", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "kestrel-mps-torch-ext", marker = "sys_platform == 'darwin'" }, + { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch-c-dlpack-ext", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d3/c4de681c3844a7c6fb485cf941328ea408291724ebd8b89c42888df1e334/kestrel_kernels-0.7.4-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:1f0e3ac8920529efc19eed366c3eba807fcb7fa0b2b3b71bb7dbfd35229d0b22", size = 1419313, upload-time = "2026-09-29T22:11:13.769Z" }, + { url = "https://files.pythonhosted.org/packages/92/78/0ac96708ca3282613b0c824a7eb12f1fff3d6f13d4868379f218fe940bf7/kestrel_kernels-0.7.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:759c8db277635c7aae25095699020806e565521423759e2b575193a6f84897fc", size = 4312046, upload-time = "2026-09-29T22:11:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/66/0a/443917cbd5a6ef5f5feb2da09298d068ce70f5d90567d7b92f7d56d26318/kestrel_kernels-0.7.4-cp312-cp312-manylinux_2_34_aarch64.manylinux_2_35_aarch64.whl", hash = "sha256:d6304ba011a996a17e4ef34f47d6653eeca330ff3a43d607d536e0a4a82114e9", size = 3974369, upload-time = "2026-09-29T22:11:16.463Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/d23e62a4091d74e518ac9ce0ca00f4ebdba35d9afc52b8998309f284cbb6/kestrel_kernels-0.7.4-cp312-cp312-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:50a8b4bea28675db48920e58df0fa0911bb5667e42f83b153e99f3cac367e6a3", size = 7492159, upload-time = "2026-09-29T22:11:18.299Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-a" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a7/c0dd106aff336f5882df9098084f5f2ad8cfbaf69d151bac4093faf7b915/kestrel_kernels_bundle_a-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:e5497b5afaf73608d565a9d446740d1196c95d1150cb9ca5f80d30d5e7d58ce3", size = 77221478, upload-time = "2026-09-29T22:08:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/e541d796565f806557cd5fd25c541b05348a5e947422ce8ae25867f98c96/kestrel_kernels_bundle_a-0.7.4-py3-none-manylinux_2_34_aarch64.manylinux_2_35_aarch64.whl", hash = "sha256:ae8cb1bb7ac56b20ada72b16c380dc83e165706f01491ad5a5537ef3038e6acf", size = 51187518, upload-time = "2026-09-29T22:08:31.438Z" }, + { url = "https://files.pythonhosted.org/packages/6f/33/b7ad0039ba2e3bc0788e4020ff3d57a31e2f914b27e584634a2d654c0d44/kestrel_kernels_bundle_a-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:a9054198c034a9f8ebfb3fb1cd15a68ae8b18a766632520ac6bde536efa78b95", size = 86658913, upload-time = "2026-09-29T22:09:38.495Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-b" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ff/7fd1f8b65afc53a4e77ef5f3d6896c00cc965e2b910377da6761f0ddd482/kestrel_kernels_bundle_b-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:d75f1d897b649b966295260dd04649de6c187512e10f2917f1b5972c967506ea", size = 77218807, upload-time = "2026-09-29T22:08:44.876Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/a224ba3a749e5e7d110f46cf7d927e6966be9f5605b24fc0b6e8b3f12141/kestrel_kernels_bundle_b-0.7.4-py3-none-manylinux_2_34_aarch64.manylinux_2_35_aarch64.whl", hash = "sha256:75b1b55ee64e49c1d54000b93e6ed9cf1c194ec322a99db3fee26ae11d512c9a", size = 31408481, upload-time = "2026-09-29T22:08:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d6/dcc1697d7fe3ca12758353c8cf1ea116ed025c33c3b397e58415340303ce/kestrel_kernels_bundle_b-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:8e954e9cabb6a57264b22ef15f7b1e178242d339211572f2b462b4139187a7f6", size = 86656107, upload-time = "2026-09-29T22:09:44.012Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-c" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/41/8bd546f50f8793820191503a2ba959e5ff80ffd9894424de9630338d11e9/kestrel_kernels_bundle_c-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:e9bdb4cb45c376d26d587d4c9270036065341c10b3e708c1217a144706580e12", size = 77226427, upload-time = "2026-09-29T22:08:50.033Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/ba67e032782bb7914e6e65231c154782cdc3a14996865e14f69df8f0f29e/kestrel_kernels_bundle_c-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:557dc5b73ef6ad150c6e8678f285f8dbf3e1b7edeb07980d2ae8880c223d88fe", size = 86663449, upload-time = "2026-09-29T22:09:49.429Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-d" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/39/f1a209873835311a17f4479085db9c3786378db38fec307dfbedc693fe42/kestrel_kernels_bundle_d-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:869b3e4321a0af2a0f4779c32bcc9bc92bc2d5fd62f26baf5e0876b9e369bad7", size = 77233940, upload-time = "2026-09-29T22:08:55.596Z" }, + { url = "https://files.pythonhosted.org/packages/b9/05/6fc8c2e16ede91ba8bf329feec38682bad8e286cd1798889585e20b1a42f/kestrel_kernels_bundle_d-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:57829cdfa7303852f94677750769c9f042fb31e13579abc421c03d5c24bb327e", size = 86653032, upload-time = "2026-09-29T22:09:54.971Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-e" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/1d/2ce40d4537fc99cf57fbc367fe74e424278582542cb7d1c6f5c8e6f4bee8/kestrel_kernels_bundle_e-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:e73adcc6fe420f4506680703a97cc9f1bfc96648477364473d6b767d43fb37e1", size = 77221432, upload-time = "2026-09-29T22:09:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/ba7570a77933c2d7eae78de7ae2271db5b6f8ec109b6e4374ccb60cb47a6/kestrel_kernels_bundle_e-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:500cf46fdb07b46516d76cc4bff7a67b9e4e78cad831443d7b908f22c3658843", size = 86656492, upload-time = "2026-09-29T22:10:00.163Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-f" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e1/1e8972ca4de997aeebc0417647e13b8889eb201b7bc1bdf505f4e87137c0/kestrel_kernels_bundle_f-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:748921b67b78d37c5577acc15430e7cbd6d39b30e7ab46696a17f83a29c72851", size = 1278, upload-time = "2026-09-29T22:09:03.14Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ae/e1e51715ba0bb6ee9dc5e25f8f87eed41699622f9e6c416fa17b91115955/kestrel_kernels_bundle_f-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:2bc734bc0c1aaa2b617c2340c461e5ebcdd163752afeb4519106694c6bbe5779", size = 86656422, upload-time = "2026-09-29T22:10:05.673Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-g" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/d9/3e623ca32548ed2db3535ad7de8161b86d2ec0691a57e58258fb0d11ea0d/kestrel_kernels_bundle_g-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:3d55eaefbae08c8ccb91298d2a84ed47eeab76f43fb72e8505839585583b3efa", size = 1278, upload-time = "2026-09-29T22:09:03.866Z" }, + { url = "https://files.pythonhosted.org/packages/42/64/6f4e3dfa67a95a84d2cabf859e70b9bbcfb55c23331997bec5536e4c6728/kestrel_kernels_bundle_g-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:7ff7e38df37f177a8273089c298d628f6f94cb6bd3c050c9c0a5f0d4177f2a54", size = 86648470, upload-time = "2026-09-29T22:10:11.903Z" }, +] + +[[package]] +name = "kestrel-kernels-bundle-h" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e5/7145290edbb208e923fbee246a9836e9c75881bbfe860e6a8552cdce7ff7/kestrel_kernels_bundle_h-0.7.4-py3-none-manylinux_2_24_x86_64.manylinux_2_31_x86_64.whl", hash = "sha256:997b2301b20d4900766b91b1ea5e5712ca3c1d6b8f4cd7018802ee4557f8d1e6", size = 1279, upload-time = "2026-09-29T22:09:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/54/4a/c1838ceecbe4df1e025a9bb392339ca5bcba584120901a24fb5093fdc907/kestrel_kernels_bundle_h-0.7.4-py3-none-manylinux_2_34_x86_64.manylinux_2_35_x86_64.whl", hash = "sha256:7ddd51d2807bd4eaddf714b89dad190b3cf2d16f61e7116c9ed23ed77d7eb33c", size = 86660329, upload-time = "2026-09-29T22:10:17.585Z" }, +] + +[[package]] +name = "kestrel-mps-torch-ext" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/05/869fb8240b4c4bdc9d61347d53ae25196e0effbdb1536a0d9368f5dfa381/kestrel_mps_torch_ext-0.1.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:2c7bd937239827dfa1e7fde2980851f308608dbc86a51970bf92cd3515fc9dae", size = 326590, upload-time = "2026-09-18T11:22:54.874Z" }, +] + +[[package]] +name = "kestrel-native" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/da/9c8faa2afe97fa74b13e7e458416ba3befe8a875a27d8e5f0c6d24320286/kestrel_native-0.1.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:682c3a42c39406de6d9aa6135932e5448e927c770e2e48200a64a595afdd506f", size = 2206004, upload-time = "2026-08-16T15:10:19.118Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/4a783a79c3433f1c3212f5617ad394de76573b4cecaff85a31cd514fc177/kestrel_native-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6278264c0c39f67689ad68a493a0da6cbb3ed6b5917cd4d812b9ac30ca539f7", size = 1858062, upload-time = "2026-08-16T15:09:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/120a1d4c67726a4667c861270782eb6ddf5049d652fcd1097a89c58c9e3e/kestrel_native-0.1.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b145e8b36d90189cf5ee58344cc70a524d41f2dd89cebe0a6c1b4d140344cbb", size = 2018843, upload-time = "2026-08-16T15:10:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/b2/61/aff186cbd587b4aa6bf67106a2731f55e5d7a497f008d6bc63efe1b97e45/kestrel_native-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa065e659bce0d3cb5989c987793eebdc494c025c20309398b7fec9f8ad3f4ea", size = 2331778, upload-time = "2026-08-16T15:10:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/22/9f/ad35eb4b36aab3c9987d365b7a85e4de9f386aaaf1343d1b53ff554566b9/kestrel_native-0.1.8-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:05fef25e9bee54e6e8ac26e34cd23bcf7162727efe02b7360ece9fb93e44ec3f", size = 1920306, upload-time = "2026-08-16T15:09:58.603Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/76/3af777226b63a5e64a6b36b1ec5855c14e2b94a37096d4760e595fc43511/networkx-3.7.tar.gz", hash = "sha256:fd77a511bd90f39f3d016351345b52cf5319b813bdca01de3f755d3cca62e96a", size = 1866482, upload-time = "2026-09-21T16:45:16.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/cd/fe58041e9011f307c490e3e17dd48cc516448f7c698a3f2d9d9d65d7e6a8/networkx-3.7-py3-none-any.whl", hash = "sha256:e3fd2c13a7814cee3746340d8d7f8598a67f16a58bf47fb7f8793fab6efca1b0", size = 2142205, upload-time = "2026-09-21T16:45:14.609Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +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.24.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/791cffd048fe5b044e620df55267e3e95c0e6e07d50b41e377c03dfc910f/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:71f181cd810e90f9b6023b01186fe82d13d65f0ec098581ee201d39fad769e4b", size = 553099438, upload-time = "2026-07-02T16:27:42.58Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +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", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +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", marker = "sys_platform == 'linux'" }, +] +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.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.4.92" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6b/eef7a9e32872b8f41e145bf10cddc9af26e153c338852811fe9a9baddf9e/nvidia_nvjitlink-13.4.92-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e0391f24ed94ec879b84e3da4d4ec320c879aff681f2c7a638462f7199284323", size = 42452378, upload-time = "2026-09-16T20:45:29.042Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a8/1cbd4014898af8b419e69b0d7dbc63da2121ee92d92b47d59f4fe9075349/nvidia_nvjitlink-13.4.92-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:25f74fad0d654271c921ac4dca614bd6258bc21791242fc7b2289dad7ae9c099", size = 40420120, upload-time = "2026-09-16T20:45:19.163Z" }, +] + +[[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.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[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 = "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/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" }, +] + +[[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" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "soundfile" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +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" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +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 = "tokenizers" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, +] + +[[package]] +name = "torch" +version = "2.14.1" +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", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { 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", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/3c/379349820e9a6e713826de3d0b88f07728541bdcabe2a3afd128871cdc4b/torch-2.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:420dbf314c180ee4b86e9bc00aee5746a7d6e5bacdd7df925af671ed393f0b2e", size = 127307099, upload-time = "2026-09-30T17:44:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3e/474930ab971e306957ec763b7df695d659179e15ccab9e7073d47ae14ce8/torch-2.14.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0b13acde294b401509830d85a8906cbf2fc97425a12a66cce1d9fa13c5106a2a", size = 453998918, upload-time = "2026-09-30T17:52:45.134Z" }, + { url = "https://files.pythonhosted.org/packages/ba/fd/c470857d0dfc5d4c94430658b3eed60fd5292b0bf596a8aae7b9fd66d674/torch-2.14.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:23011fe29a99b591eabb3a31a25080c62e2c2f0690a3c798744e5489dae50451", size = 554618695, upload-time = "2026-09-30T17:53:06.8Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/67/10d236698525d7b7db4d74ec0a4b01f5b2db33968995fdd9ac6b4635e327/torch_c_dlpack_ext-0.1.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c0f2bd51fcd99c0e5b50314e1985f2728c4941bfa821f065e6c30951d1f995ca", size = 5291237, upload-time = "2026-01-12T11:24:44.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" }, +] + +[[package]] +name = "transcribe-parakeet-kestrel-env" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "kestrel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "kestrel-kernels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "kestrel-native", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "soundfile", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] + +[package.metadata] +requires-dist = [ + { name = "huggingface-hub", specifier = ">=0.20" }, + { name = "kestrel", specifier = "==0.9.1" }, + { name = "kestrel-kernels", specifier = "==0.7.4" }, + { name = "kestrel-native", specifier = "==0.1.8" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "soundfile", specifier = ">=0.12" }, + { name = "torch", specifier = ">=2.8" }, +] + +[[package]] +name = "triton" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/07/0f8cd8e8db0472334253efdaaab3d0819fea27aa99bf0e7f1aeea4ceb5ae/triton-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9c404c69ed4a39e8ec632eaf6b9fe058a060bf98979c177f6ef666f06bb8d50", size = 226474486, upload-time = "2026-08-28T16:08:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/b7012e5bfae67640f268aa584caa80fe1674f6b0da949046b679972c33e3/triton-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91ffa46d095b252248297292dd22bcbacd53a125a0c2eefbbbf74925a320bc3", size = 247972921, upload-time = "2026-08-28T15:55:53.157Z" }, +] + +[[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" }, +] diff --git a/scripts/hf_cards/parakeet-ultra.yaml b/scripts/hf_cards/parakeet-ultra.yaml new file mode 100644 index 000000000..cb7ed8b63 --- /dev/null +++ b/scripts/hf_cards/parakeet-ultra.yaml @@ -0,0 +1,45 @@ +# Spec for the HF README of handy-computer/parakeet-ultra-gguf. +# Prose only; numbers and metadata come from catalog/.json. See README.md. + +pin_date: 2026-10-04 + +validation: + reference: kestrel 0.9.1 (Photon) + commit: 08f9e6f0 + date: 2026-10-05 + +pipeline_tag: automatic-speech-recognition +tags: + - gguf + - transcribe.cpp + - asr + - speech-to-text + - parakeet + - conformer + - tdt + - multilingual + - long-form + +summary: | + Moondream's post-trained parakeet-tdt-0.6b-v3. Offline multilingual + speech-to-text over the same 25 European languages, with lower error rates + than v3 on English and on every FLEURS language we measured. A FastConformer + encoder with a TDT transducer decoder, plus a small voice-activity head that + the model uses to cut long recordings at pauses into segments of at most + 30 seconds. Takes 16 kHz mono WAV and produces a punctuated, cased + transcript with optional token-level timestamps. A drop-in replacement for + v3: same size, languages and API. Not a streaming model and does not + translate. + +wer: + notes: | + Greedy transducer decoding, no external LM. The reference is Moondream's + own runtime (kestrel 0.9.1, the engine behind Photon), not transformers. + On this manifest kestrel scores 1.80% and the C++ F32 build matches it. + + Moondream's card reports 1.41% on the same split, scored with the Open + ASR Leaderboard normalizer. + + Long-form: on 11 full TED-LIUM 3 talks, segmented by the model's + voice-activity head exactly as kestrel does it, F32 scores 2.22%, identical + to kestrel, and every quant stays within 0.05pp. diff --git a/scripts/lib/gguf_common.py b/scripts/lib/gguf_common.py index 98091f6f3..1712935ae 100644 --- a/scripts/lib/gguf_common.py +++ b/scripts/lib/gguf_common.py @@ -212,7 +212,9 @@ def reference_dtype_for( and "enc.blocks." in name ) is_conv = ".conv." in name and name.endswith(".weight") - if is_convpw or is_conv: + # parakeet-ultra VAD head kernels (vad.{proj,ctx,out}.weight). + is_vad = name.startswith("vad.") and name.endswith(".weight") + if is_convpw or is_conv or is_vad: if reference_type == GGMLQuantizationType.BF16: return GGMLQuantizationType.F16 # loader has no BF16 conv kernel return reference_type diff --git a/scripts/lib/test_quant_policy_sync.py b/scripts/lib/test_quant_policy_sync.py index 2b6cda9c6..b3d5b9482 100644 --- a/scripts/lib/test_quant_policy_sync.py +++ b/scripts/lib/test_quant_policy_sync.py @@ -64,6 +64,7 @@ # buffers. Loader requires F32 in these slots -> F32 at every reference dtype. NORM = [ "dec.layers.0.attn.linear_q.bias", # .bias + "vad.ctx.bias", # parakeet-ultra VAD head bias "enc.blocks.3.norm_ff1.weight", # norm_ prefix "enc.blocks.3.conv.bn.weight", # .bn. batchnorm "dec.final_norm.weight", # cohere final norm (dot separator) @@ -104,6 +105,8 @@ "enc.blocks.3.conv.pointwise2.weight", # conformer 1x1 pointwise "enc.pre_encode.conv.0.weight", # pre-encode subsampling conv "enc.blocks.3.conv.depthwise.weight", # conformer depthwise conv + "vad.proj.weight", # parakeet-ultra VAD head 1x1 conv + "vad.ctx.weight", # parakeet-ultra VAD head k=5 conv ] # Linear / Embed: ggml_mul_mat operands and the decoder token embedding. diff --git a/scripts/validate.py b/scripts/validate.py index 43af1e3fb..6b5950c64 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -153,6 +153,24 @@ def case_language(case) -> str | None: return "en" +def case_stages(case, default: list[str]) -> list[str]: + """Per-case dumper subcommands. Dict cases may set `stages` (e.g. a + `longform` path instead of whole-clip encoder/decode).""" + if isinstance(case, dict) and "stages" in case: + stages = case["stages"] + if not isinstance(stages, list) or not all(isinstance(s, str) for s in stages): + raise SystemExit(f"error: case stages must be a list of strings: {case!r}") + return list(stages) + return list(default) + + +def manifest_env_dir(repo: Path, manifest: dict[str, Any], family: str) -> Path: + """Reference env: scripts/envs/, else scripts/envs/. + A variant whose reference framework differs from the family's names its own.""" + env = (manifest.get("reference") or {}).get("env") or family + return repo / "scripts" / "envs" / str(env) + + def case_transcript_compare(manifest: dict[str, Any], case) -> str: value = manifest.get("transcript_compare", "exact") if isinstance(case, dict) and "transcript_compare" in case: @@ -347,7 +365,7 @@ def cmd_ref(args: argparse.Namespace) -> int: raise SystemExit("error: no model specified and none in manifest") dump_script = manifest_dump_script(repo, manifest) - env_dir = repo / "scripts" / "envs" / args.family + env_dir = manifest_env_dir(repo, manifest, args.family) cases = manifest.get("cases", ["jfk"]) for case in cases: @@ -414,7 +432,7 @@ def cmd_ref(args: argparse.Namespace) -> int: if args.family == "sortformer": stages = ["encoder", "diarize"] else: - stages = ["encoder", "decode"] + stages = case_stages(case, ["encoder", "decode"]) sf_preset = os.environ.get("VALIDATE_SORTFORMER_PRESET") for stage in stages: stage_args = list(common_args) @@ -454,6 +472,9 @@ def cmd_cpp(args: argparse.Namespace) -> int: env = os.environ.copy() env["TRANSCRIBE_DUMP_DIR"] = str(out_dir) + # Manifest-declared C++ env for the correctness regime (e.g. NO_FLASH). + for key, value in (manifest.get("cpp_env") or {}).items(): + env[str(key)] = str(value) # Sortformer: keep the C++ streaming operating point in lockstep with # the reference `diarize --preset` (see cmd_ref) so the diar.probs @@ -736,6 +757,9 @@ def cmd_mel(args: argparse.Namespace) -> int: env = os.environ.copy() env["TRANSCRIBE_DUMP_DIR"] = str(out_dir) + # Manifest-declared C++ env for the correctness regime (e.g. NO_FLASH). + for key, value in (manifest.get("cpp_env") or {}).items(): + env[str(key)] = str(value) env.pop("TRANSCRIBE_MEL_FROM_REF", None) cmd = [ diff --git a/scripts/wer/ingest.py b/scripts/wer/ingest.py index fe03c4202..4e94af0e9 100644 --- a/scripts/wer/ingest.py +++ b/scripts/wer/ingest.py @@ -322,12 +322,57 @@ def ingest_eka_medical_asr(repo: Path, args: argparse.Namespace) -> int: return 0 +# -------- TED-LIUM 3 long-form (11 full test talks) ----------------------- +# +# https://huggingface.co/datasets/distil-whisper/tedlium-long-form +# Full recordings (10-20 min) with concatenated, lowercased references. +# Schema: `audio`, `text`, `speaker_id`. No clip is under 30 s. + +def ingest_tedlium_longform(repo: Path, args: argparse.Namespace) -> int: + out_dir = repo / "samples/wer/tedlium-longform" + manifest = repo / "samples/wer/tedlium-longform.manifest.jsonl" + + if manifest.exists() and not args.force: + n_existing = sum(1 for _ in open(manifest)) + print(f"OK already exists: {manifest} ({n_existing} talks). " + f"Pass --force to regenerate.") + return 0 + + print("loading distil-whisper/tedlium-long-form split=test") + from datasets import load_dataset + + ds = load_dataset("distil-whisper/tedlium-long-form", split="test") + out_dir.mkdir(parents=True, exist_ok=True) + + entries: list[dict] = [] + for row in ds: + utt_id = f"tedlium-longform-{row['speaker_id']}" + wav_path = out_dir / f"{utt_id}.wav" + if not wav_path.exists(): + audio = row["audio"] + write_wav_16k_mono(np.asarray(audio["array"], dtype=np.float32), + int(audio["sampling_rate"]), wav_path) + entries.append({ + "id": utt_id, + "audio": str(wav_path), + "ref_text": " ".join(row["text"].split()), + "language": "en", + }) + + entries.sort(key=lambda e: e["id"]) + write_manifest(entries, manifest) + print(f"manifest: {manifest}") + print(f" {len(entries)} talks") + return 0 + + # -------- Dispatch -------------------------------------------------------- SOURCES = { "librispeech": ingest_librispeech, "fleurs": ingest_fleurs, "eka-medical-asr": ingest_eka_medical_asr, + "tedlium-longform": ingest_tedlium_longform, } @@ -339,7 +384,7 @@ def main() -> int: formatter_class=argparse.RawDescriptionHelpFormatter, ) sub = p.add_subparsers(dest="source", required=True, - metavar="{librispeech,fleurs,eka-medical-asr}") + metavar="{librispeech,fleurs,eka-medical-asr,tedlium-longform}") p_ls = sub.add_parser("librispeech", help="LibriSpeech split (English-only).") @@ -369,6 +414,12 @@ def main() -> int: p_ek.add_argument("--force", action="store_true", help="Regenerate even if manifest already exists.") + p_tl = sub.add_parser("tedlium-longform", + help="distil-whisper/tedlium-long-form: the 11 full " + "TED-LIUM 3 test talks (long-form).") + p_tl.add_argument("--force", action="store_true", + help="Regenerate even if manifest already exists.") + args = p.parse_args() return SOURCES[args.source](repo, args) diff --git a/scripts/wer/run_reference_parakeet_kestrel.py b/scripts/wer/run_reference_parakeet_kestrel.py new file mode 100644 index 000000000..c15d5555a --- /dev/null +++ b/scripts/wer/run_reference_parakeet_kestrel.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +run_reference_parakeet_kestrel.py — parakeet-ultra reference WER via kestrel. + +Transcribes a WER manifest through kestrel's ParakeetTdtRuntime (clips over +30 s are VAD-segmented and stitched). Writes run.py-compatible JSONL for +scripts/wer/score.py. + +Mirrors scripts/dump_reference_parakeet_kestrel.py: float32 weights, no +language hint (kestrel rejects language forcing). + +Usage (from repo root): + + uv run --project scripts/envs/parakeet-kestrel \\ + scripts/wer/run_reference_parakeet_kestrel.py \\ + --manifest samples/wer/librispeech-test-clean.manifest.jsonl \\ + --model moondream/parakeet-ultra \\ + --revision 73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b \\ + --out reports/wer/parakeet-ultra-REF.librispeech-test-clean.jsonl +""" + +from __future__ import annotations + +import argparse +import json +import sys +import time +import types +from importlib.metadata import version +from pathlib import Path + +ULTRA_REVISION = "73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b" + + +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).") + p.add_argument("--out", type=Path, required=True, + help="Output JSONL path (run.py-compatible).") + p.add_argument("--model", default="moondream/parakeet-ultra", + help="HF repo id or local snapshot dir.") + p.add_argument("--revision", default=ULTRA_REVISION, + help="HF revision (kestrel itself tracks main; pin it).") + p.add_argument("--device", default="cpu", choices=("cpu", "mps", "cuda"), + help="Torch device (default cpu).") + p.add_argument("--batch-size", type=int, default=1, + help="Requests per runtime.forward call. 1 keeps every " + "utterance on the single-row decode path (default).") + p.add_argument("--torch-threads", type=int, default=0, + help="CPU threads (0 = kestrel's default).") + 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 soundfile as sf + import torch + from kestrel.models.parakeet_tdt.runtime import ParakeetTdtRuntime + from kestrel.models.parakeet_tdt.weights import load_parakeet_tdt + + t0 = time.monotonic() + loaded = load_parakeet_tdt(args.model, revision=args.revision, + device=args.device, dtype=torch.float32) + cfg = types.SimpleNamespace( + model=args.model, device=args.device, dtype=torch.float32, + cpu_threads=args.torch_threads or None, enable_cuda_graphs=False, + ) + runtime = ParakeetTdtRuntime(cfg, model=loaded.model, tokenizer=loaded.tokenizer) + load_ms = (time.monotonic() - t0) * 1000 + print(f"loaded {args.model}@{args.revision[:12]} on {args.device} " + f"(vad_head={loaded.model.vad_head is not None}) in {load_ms:.0f} ms", flush=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}") + # Kestrel takes no language hint; the manifest's language is recorded + # only so score.py picks the right normalizer (absent means English). + languages = {entry.get("language") for entry in manifest} + language = languages.pop() if len(languages) == 1 else None + + n_done = n_errors = 0 + audio_s = 0.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": "kestrel", + "framework_version": f"kestrel=={version('kestrel')}, kestrel-kernels=={version('kestrel-kernels')}", + "model": args.model, + "model_revision": args.revision, + "device": args.device, + "batch_size": args.batch_size, + "language": language, + }) + "\n") + fout.flush() + + for start in range(0, total, args.batch_size): + group = manifest[start:start + args.batch_size] + requests = [] + for entry in group: + pcm, sr = sf.read(entry["audio"], dtype="float32", always_2d=False) + audio_s += pcm.size / sr + requests.append({"audio": pcm, "sample_rate": sr, "timestamps": "none"}) + t_start = time.monotonic() + try: + results = runtime.forward("transcribe", requests) + except Exception as e: # whole-cohort failure + results = [e] * len(group) + elapsed_ms = round((time.monotonic() - t_start) * 1000 / len(group), 1) + for entry, result in zip(group, results): + err, hyp = "", "" + if isinstance(result, Exception): + err = f"{type(result).__name__}: {result}" + n_errors += 1 + else: + hyp = str(result["text"]) + fout.write(json.dumps({ + "id": entry["id"], + "ref_text": entry.get("ref_text", ""), + "hyp_text": hyp.strip(), + "raw_text": hyp, + "mel_ms": 0, + "encode_ms": 0, + "decode_ms": elapsed_ms, + "latency_ms": elapsed_ms, + "error": err, + }) + "\n") + n_done += 1 + fout.flush() + if n_done % 100 < len(group) 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, xRT {audio_s / wall:.1f}, " + f"ETA {eta / 60:.1f} min, errors={n_errors}", flush=True) + + wall = time.monotonic() - t_loop + print(f"\ndone. {n_done} utterances, {audio_s / 3600:.2f} h audio in {wall:.1f}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/src/CMakeLists.txt b/src/CMakeLists.txt index 5b0931f71..05575a7da 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(transcribe causal_lm/causal_lm.cpp granite_conformer/shaw_attn.cpp arch/parakeet/model.cpp + arch/parakeet/longform.cpp arch/parakeet/multitalker.cpp arch/parakeet/capabilities.cpp arch/parakeet/weights.cpp diff --git a/src/arch/parakeet/decoder.cpp b/src/arch/parakeet/decoder.cpp index c52f295ac..52defc38f 100644 --- a/src/arch/parakeet/decoder.cpp +++ b/src/arch/parakeet/decoder.cpp @@ -766,10 +766,11 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, HostDe } // ----- TDT params ----- - out.tdt_durations = hp.tdt_durations; - out.tdt_max_symbols = hp.tdt_max_symbols; - out.n_vocab = hp.pred_vocab - 1; // raw SP vocab size - out.blank_id = hp.pred_vocab - 1; // blank lives at vocab_size + out.tdt_durations = hp.tdt_durations; + out.tdt_max_symbols = hp.tdt_max_symbols; + out.tdt_global_symbol_budget = hp.tdt_global_symbol_budget; + out.n_vocab = hp.pred_vocab - 1; // raw SP vocab size + out.blank_id = hp.pred_vocab - 1; // blank lives at vocab_size return TRANSCRIBE_OK; } @@ -1096,7 +1097,11 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, // the unconditional path. bool predictor_dirty = true; - while (step < T_enc && iter < max_iters) { + // See ParakeetHParams::tdt_global_symbol_budget. + const bool global_budget = w.tdt_global_symbol_budget; + int64_t steps_remaining = static_cast(w.tdt_max_symbols) * T_enc; + + while (step < T_enc && iter < max_iters && (!global_budget || steps_remaining > 0)) { ++iter; // ----- Predictor (one LSTM step) ----- @@ -1169,6 +1174,12 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, predictor_dirty = true; } + if (global_budget) { + step += (is_blank && duration == 0) ? 1 : duration; + --steps_remaining; + continue; + } + // Step / stuck advance. Matches the reference: // step += duration // new_symbols += 1 @@ -1204,7 +1215,7 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, t_joint_us / 1000.0, t_conf_us / 1000.0, (t_enc_proj_us + t_pred_us + t_joint_us + t_conf_us) / 1000.0, static_cast(t_pred_us + t_joint_us) / std::max(iter, 1)); - if (iter >= max_iters) { + if (iter >= max_iters && !global_budget) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: hit iteration cap (%d) — pathological " "logits or loop bug", diff --git a/src/arch/parakeet/decoder.h b/src/arch/parakeet/decoder.h index e4de9dfa3..e7cb70a55 100644 --- a/src/arch/parakeet/decoder.h +++ b/src/arch/parakeet/decoder.h @@ -162,13 +162,15 @@ enum class HostHeadKind { TDT, RNNT, CTC }; // cap is reused. struct HostDecoderWeights { HostHeadKind head_kind = HostHeadKind::TDT; - HostPredictor predictor; // empty for CTC - HostJoint joint; // empty for CTC - HostCtcHead ctc_head; // empty for TDT/RNNT - std::vector tdt_durations; // empty for RNNT/CTC - int tdt_max_symbols = 0; - int blank_id = 0; // unified: TDT/RNNT == pred_vocab - 1; CTC == ctc_head.blank_id - int n_vocab = 0; // raw SP vocab size (excludes blank) + HostPredictor predictor; // empty for CTC + HostJoint joint; // empty for CTC + HostCtcHead ctc_head; // empty for TDT/RNNT + std::vector tdt_durations; // empty for RNNT/CTC + int tdt_max_symbols = 0; + // See ParakeetHParams::tdt_global_symbol_budget. + bool tdt_global_symbol_budget = false; + int blank_id = 0; // unified: TDT/RNNT == pred_vocab - 1; CTC == ctc_head.blank_id + int n_vocab = 0; // raw SP vocab size (excludes blank) }; // Build host mirrors from a loaded ParakeetModel. Reads tensor bytes diff --git a/src/arch/parakeet/encoder.cpp b/src/arch/parakeet/encoder.cpp index 06cd50595..1e1861e68 100644 --- a/src/arch/parakeet/encoder.cpp +++ b/src/arch/parakeet/encoder.cpp @@ -323,17 +323,19 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, if (n_batch < 1) { n_batch = 1; } - const bool var_len_masks = batch_var_len && n_batch > 1; + const bool var_len_masks = batch_var_len && (n_batch > 1 || hp.kestrel_length_masking); conf::ConvPolicy policy{}; - policy.direct_pw = conf::detect_direct_pw(backend_name); - policy.direct_conv0_in_pre_encode = true; - policy.direct_dw_in_block = detect_direct_dw_in_block(backend_name); - policy.direct_dw_in_pre_encode = detect_direct_dw_in_pre_encode(backend_name); + policy.pre_encode_mask_after_stride = hp.kestrel_length_masking; + policy.pre_encode_f32_pointwise = hp.kestrel_length_masking; + policy.direct_pw = conf::detect_direct_pw(backend_name); + policy.direct_conv0_in_pre_encode = true; + policy.direct_dw_in_block = detect_direct_dw_in_block(backend_name); + policy.direct_dw_in_pre_encode = detect_direct_dw_in_pre_encode(backend_name); // Cache-aware streaming (NeMo causal_downsampling=true) uses // CausalConv2D for the pre-encode subsample (left=k-1, right=stride-1). // Inferred from the attention style — only ChunkedLimited is causal. // Independent of the conformer conv-module's conv_context. - policy.causal_pre_encode = (hp.enc_att_context_style == ParakeetHParams::AttContextStyle::ChunkedLimited); + policy.causal_pre_encode = (hp.enc_att_context_style == ParakeetHParams::AttContextStyle::ChunkedLimited); EncoderBuild eb{}; @@ -385,9 +387,11 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, /*name_prefix=*/"enc.pre_encode", /*error_tag=*/"parakeet", mask_pre_encode ? &pe_masks : nullptr); if (mask_pre_encode) { - eb.pre_encode_mask_s1_in = pe_masks.mask_s1; - eb.pre_encode_mask_s2_in = pe_masks.mask_s2; - eb.pre_encode_mask_s3_in = pe_masks.mask_s3; + eb.pre_encode_mask_s1_in = pe_masks.mask_s1; + eb.pre_encode_mask_s2_in = pe_masks.mask_s2; + eb.pre_encode_mask_s3_in = pe_masks.mask_s3; + eb.pre_encode_extent_s2_in = pe_masks.mask_s2_extent; + eb.pre_encode_extent_s3_in = pe_masks.mask_s3_extent; } if (x == nullptr) { // build_pre_encode already logged the diagnostic. @@ -982,4 +986,77 @@ EncoderBuild build_encoder_graph_streaming(ggml_context * ctx, return eb; } +VadBuild build_vad_graph(ggml_context * ctx, + const ParakeetWeights & w, + const ParakeetHParams & hp, + int n_mel_frames, + const char * backend_name) { + VadBuild vb{}; + if (ctx == nullptr || n_mel_frames <= 0 || !hp.has_vad_head || w.vad.proj_w == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet vad: invalid arg (no vad head or empty mel)"); + return vb; + } + // Same subsampler policy as build_encoder_graph on this model, so the + // head sees exactly the pre_encode output the encoder would. + conf::ConvPolicy policy{}; + policy.direct_pw = conf::detect_direct_pw(backend_name); + policy.direct_conv0_in_pre_encode = true; + policy.direct_dw_in_block = detect_direct_dw_in_block(backend_name); + policy.direct_dw_in_pre_encode = detect_direct_dw_in_pre_encode(backend_name); + policy.causal_pre_encode = false; + policy.pre_encode_mask_after_stride = hp.kestrel_length_masking; + policy.pre_encode_f32_pointwise = hp.kestrel_length_masking; + + vb.mel_in = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, n_mel_frames, hp.fe_num_mels, 1, 1); + ggml_set_name(vb.mel_in, "vad.mel.in"); + ggml_set_input(vb.mel_in); + + conf::PreEncodeValidMasks pe_masks; + ggml_tensor * x = + conf::build_pre_encode(ctx, to_view(w.pre_encode), vb.mel_in, policy, /*name_prefix=*/"vad.pre_encode", + /*error_tag=*/"parakeet vad", hp.kestrel_length_masking ? &pe_masks : nullptr); + if (x == nullptr) { + return vb; + } + vb.pe_mask_s1_in = pe_masks.mask_s1; + vb.pe_mask_s2_in = pe_masks.mask_s2; + vb.pe_mask_s3_in = pe_masks.mask_s3; + vb.pe_extent_s2_in = pe_masks.mask_s2_extent; + vb.pe_extent_s3_in = pe_masks.mask_s3_extent; + vb.pre_encode_out = x; // [d_model, T] + + const int64_t d_model = x->ne[0]; + const int64_t H = hp.vad_hidden; + const int K = hp.vad_context_kernel; + + // proj: Conv1d(d_model -> H, k=1) == mul_mat over channels. [1, d, H] -> [d, H]. + ggml_tensor * h = ggml_mul_mat(ctx, ggml_reshape_2d(ctx, w.vad.proj_w, d_model, H), x); // [H, T] + h = ggml_add(ctx, h, w.vad.proj_b); + h = ggml_silu(ctx, h); + vb.proj_out = conf::named(h, "vad.proj"); + + // conv_1d_f32 takes [T, IC] data and returns [T, OC]. + ggml_tensor * t = ggml_cont(ctx, ggml_transpose(ctx, h)); // [T, H] + t = conf::conv_1d_f32(ctx, w.vad.ctx_w, t, /*stride=*/1, /*padding=*/K / 2, /*dilation=*/1); // [T, H] + t = ggml_add(ctx, t, ggml_reshape_2d(ctx, w.vad.ctx_b, 1, H)); + t = ggml_silu(ctx, t); + ggml_tensor * c = ggml_cont(ctx, ggml_transpose(ctx, t)); // [H, T] + vb.ctx_out = conf::named(c, "vad.ctx"); + + ggml_tensor * o = ggml_mul_mat(ctx, ggml_reshape_2d(ctx, w.vad.out_w, H, 1), c); // [1, T] + o = ggml_add(ctx, o, w.vad.out_b); + vb.prob = conf::named(ggml_sigmoid(ctx, o), "vad.prob"); + + vb.graph = ggml_new_graph_custom(ctx, /*size=*/1024, /*grads=*/false); + if (vb.graph == nullptr) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet vad: ggml_new_graph_custom failed"); + return vb; + } + ggml_build_forward_expand(vb.graph, vb.prob); + for (ggml_tensor * keep : { vb.pre_encode_out, vb.proj_out, vb.ctx_out }) { + transcribe::debug::mark_tensor_for_dump(keep); + } + return vb; +} + } // namespace transcribe::parakeet diff --git a/src/arch/parakeet/encoder.h b/src/arch/parakeet/encoder.h index 7105e9b1f..921a58a5d 100644 --- a/src/arch/parakeet/encoder.h +++ b/src/arch/parakeet/encoder.h @@ -130,9 +130,11 @@ struct EncoderBuild { // (1 on valid time frames, 0 on padded). Null unless variable-length // batching on a non-causal pre-encode. The driver fills them from the // per-utterance valid length downsampled to each stage. - ggml_tensor * pre_encode_mask_s1_in = nullptr; // after relu0 - ggml_tensor * pre_encode_mask_s2_in = nullptr; // after relu3 - ggml_tensor * pre_encode_mask_s3_in = nullptr; // after relu6 + ggml_tensor * pre_encode_mask_s1_in = nullptr; // after relu0 + ggml_tensor * pre_encode_mask_s2_in = nullptr; // after relu3 + ggml_tensor * pre_encode_mask_s3_in = nullptr; // after relu6 + ggml_tensor * pre_encode_extent_s2_in = nullptr; // kestrel masking, batched (see PreEncodeValidMasks) + ggml_tensor * pre_encode_extent_s3_in = nullptr; // Multitalker speaker-kernel supervision inputs, ne=[1, T_enc] f32 // each. Null unless build_encoder_graph was called with @@ -227,6 +229,28 @@ EncoderBuild build_encoder_graph(ggml_context * compute_ctx, // the conformer blocks. 0 = no gather. int mt_keep_frames = 0); +// Speech-head graph: pre_encode subsampler, then the VAD head. One utterance +// (a scan block); the driver fills the subsampler masks like the encoder's. +struct VadBuild { + ggml_tensor * mel_in = nullptr; // ne=[T_mel, n_mels, 1, 1] + ggml_tensor * pe_mask_s1_in = nullptr; + ggml_tensor * pe_mask_s2_in = nullptr; + ggml_tensor * pe_mask_s3_in = nullptr; + ggml_tensor * pe_extent_s2_in = nullptr; // single utterance: filled all ones + ggml_tensor * pe_extent_s3_in = nullptr; + ggml_tensor * pre_encode_out = nullptr; // ne=[d_model, T] + ggml_tensor * proj_out = nullptr; // ne=[H, T], after SiLU + ggml_tensor * ctx_out = nullptr; // ne=[H, T], after SiLU + ggml_tensor * prob = nullptr; // ne=[1, T], sigmoid + ggml_cgraph * graph = nullptr; +}; + +VadBuild build_vad_graph(ggml_context * compute_ctx, + const ParakeetWeights & w, + const ParakeetHParams & hp, + int n_mel_frames, + const char * backend_name); + // Per-layer streaming cache I/O for the streaming encoder graph. // The inputs are persistent backend tensors (allocated outside the // per-call compute_ctx) holding the previous chunk's caches; the diff --git a/src/arch/parakeet/longform.cpp b/src/arch/parakeet/longform.cpp new file mode 100644 index 000000000..ee0ad53ca --- /dev/null +++ b/src/arch/parakeet/longform.cpp @@ -0,0 +1,191 @@ +// Parakeet long-form segmenter. See longform.h. + +#include "longform.h" + +#include +#include + +namespace transcribe::parakeet::longform { + +namespace { + +// A pause source reports region edges on its own frame grid, so a gap of +// exactly min_pause comes out as a difference of two multiples of that +// frame (281.0 - 280.8 is 0.19999999999998863); ties go to the pause. +constexpr double kPauseEpsilon = 1e-6; + +int64_t samples_for(double seconds, int rate) { + return round_half_even(seconds * static_cast(rate)); +} + +// segment._has_speech: whether any speech falls in [0, seconds). +bool has_speech(const Regions & regions, double seconds) { + for (const auto & r : regions) { + if (r.second > 0.0 && r.first < seconds) { + return true; + } + } + return false; +} + +} // namespace + +int64_t round_half_even(double x) { + // std::nearbyint honours the current rounding mode; the default + // (FE_TONEAREST) is ties-to-even, Python's round(). + return static_cast(std::nearbyint(x)); +} + +Regions speech_regions(const float * probs, int n_frames, const Params & p) { + // edges = flatnonzero(diff(r_[0, speaking, 0])) -> (start, end) frame pairs. + Regions regions; + int run_start = -1; + for (int i = 0; i <= n_frames; ++i) { + const bool speaking = i < n_frames && probs[i] >= p.speech_threshold; + if (speaking && run_start < 0) { + run_start = i; + } else if (!speaking && run_start >= 0) { + const double start = static_cast(run_start) * p.frame_seconds; + const double end = static_cast(i) * p.frame_seconds; + if (!regions.empty() && start - regions.back().second < p.min_gap_seconds) { + regions.back().second = end; + } else { + regions.emplace_back(start, end); + } + run_start = -1; + } + } + Regions kept; + for (const auto & r : regions) { + if (r.second - r.first >= p.min_speech_seconds) { + kept.push_back(r); + } + } + return kept; +} + +Regions pauses_from_speech(Regions speech, double duration, const Params & p) { + const double least = p.min_pause_seconds - kPauseEpsilon; + std::sort(speech.begin(), speech.end()); + Regions pauses; + double previous = 0.0; + for (const auto & r : speech) { + if (r.first - previous >= least) { + pauses.emplace_back(previous, r.first); + } + previous = std::max(previous, r.second); + } + if (duration - previous >= least) { + pauses.emplace_back(previous, duration); + } + return pauses; +} + +double next_cut(const Regions & pauses, const Params & p) { + // Last pause fully inside (min_segment, max_segment), else any pause whose + // midpoint lands there, else the cap. + const std::pair * last = nullptr; + for (const auto & r : pauses) { + if (r.first >= p.min_segment_seconds && r.second <= p.max_segment_seconds) { + last = &r; + } + } + if (last == nullptr) { + for (const auto & r : pauses) { + const double mid = (r.first + r.second) / 2; + if (p.min_segment_seconds <= mid && mid <= p.max_segment_seconds) { + last = &r; + } + } + } + if (last == nullptr) { + return p.max_segment_seconds; + } + return (last->first + last->second) / 2; +} + +bool fits_one_segment(int64_t n_samples, const Params & p) { + return n_samples <= samples_for(p.max_segment_seconds, p.sample_rate); +} + +std::vector scan_block_sizes(int64_t n_samples, const Params & p) { + const int64_t max_frames = samples_for(p.scan_block_seconds, p.sample_rate); + const int64_t window_frames = std::max(4, samples_for(p.boundary_window_secs, p.sample_rate)); + const int64_t min_tail_frames = std::max(window_frames, samples_for(p.min_tail_seconds, p.sample_rate)); + std::vector sizes; + int64_t emitted = 0; + while (emitted < n_samples) { + const int64_t remaining = n_samples - emitted; + // With boundary_search_seconds=0 the quiet-boundary search window is + // empty, so a full block is shortened only to keep min_tail behind it. + const int64_t boundary = + remaining <= max_frames ? remaining : std::min(max_frames, remaining - min_tail_frames); + sizes.push_back(boundary); + emitted += boundary; + } + return sizes; +} + +transcribe_status pause_segments(int64_t n_samples, + const Params & p, + const SpeechFn & speech, + std::vector & out) { + out.clear(); + if (n_samples <= 0) { + return TRANSCRIBE_OK; + } + const double rate = static_cast(p.sample_rate); + if (fits_one_segment(n_samples, p)) { + out.push_back({ 0, n_samples }); + return TRANSCRIBE_OK; + } + const int64_t cap_samples = samples_for(p.max_segment_seconds, p.sample_rate); + + // The carry is [consumed, consumed + carry_len) of the file; regions + // are relative to its start. + Regions regions; + int64_t consumed = 0; + int64_t carry_len = 0; + int64_t block_start = 0; + int cuts = 0; + for (const int64_t block_len : scan_block_sizes(n_samples, p)) { + const double offset = static_cast(carry_len) / rate; + Regions block_regions; + if (const transcribe_status st = speech(block_start, block_len, block_regions); st != TRANSCRIBE_OK) { + return st; + } + for (const auto & r : block_regions) { + regions.emplace_back(r.first + offset, r.second + offset); + } + carry_len += block_len; + block_start += block_len; + while (carry_len > cap_samples) { + const double cut = next_cut(pauses_from_speech(regions, static_cast(carry_len) / rate, p), p); + const int64_t index = round_half_even(cut * rate); + const double seconds = static_cast(index) / rate; + if (has_speech(regions, seconds)) { + out.push_back({ consumed, index }); + } + consumed += index; + carry_len -= index; + ++cuts; + Regions shifted; + for (const auto & r : regions) { + if (r.second > seconds) { + shifted.emplace_back(std::max(r.first - seconds, 0.0), r.second - seconds); + } + } + regions.swap(shifted); + } + } + // A remainder too short to normalize, or with no speech, left behind by + // a cut carries nothing and is dropped. + if (cuts > 0 && + (carry_len < p.min_feature_samples || !has_speech(regions, static_cast(carry_len) / rate))) { + return TRANSCRIBE_OK; + } + out.push_back({ consumed, carry_len }); + return TRANSCRIBE_OK; +} + +} // namespace transcribe::parakeet::longform diff --git a/src/arch/parakeet/longform.h b/src/arch/parakeet/longform.h new file mode 100644 index 000000000..0c539c8df --- /dev/null +++ b/src/arch/parakeet/longform.h @@ -0,0 +1,84 @@ +// Long-form pause segmentation for parakeet checkpoints that carry a VAD +// head. Port of kestrel 0.9.1 segment.py / vad.py. +// +// Audio longer than max_segment_seconds is cut at the midpoint of the last +// pause (a gap of at least min_pause_seconds between speech regions) that +// lies inside (min_segment_seconds, max_segment_seconds) of the running +// carry, else at the cap; segments are contiguous, never overlap, and a +// segment with no speech is skipped. Speech regions come from the VAD head, +// scanned one block of scan_block_seconds at a time. +// +// All time arithmetic is float64 and cut indices use round-half-to-even, +// as in the Python reference, so cut points match sample for sample. +// Host-only; no ggml. + +#pragma once + +#include "transcribe.h" + +#include +#include +#include +#include + +namespace transcribe::parakeet::longform { + +// [start, end) in seconds. +using Regions = std::vector>; + +struct Params { + int sample_rate = 16000; + double frame_seconds = 0.08; // hop / sample_rate * subsampling_factor + float speech_threshold = 0.5f; + double min_speech_seconds = 0.1; + double min_gap_seconds = 0.1; + double max_segment_seconds = 30.0; + double min_segment_seconds = 1.0; + double min_pause_seconds = 0.2; + double scan_block_seconds = 120.0; + // AudioChunks constants: a block is never followed by a tail shorter + // than max(window, 0.5 s). + double boundary_window_secs = 0.1; + double min_tail_seconds = 0.5; + // parakeet_features refuses to normalize anything shorter (samples). + int min_feature_samples = 320; +}; + +struct Segment { + int64_t start = 0; // first sample + int64_t n_samples = 0; +}; + +// Python's round() on a float: nearest integer, ties to even. +int64_t round_half_even(double x); + +// vad.speech_regions: runs of frames with p >= threshold, gaps shorter +// than min_gap bridged, runs shorter than min_speech dropped. +Regions speech_regions(const float * probs, int n_frames, const Params & p); + +// segment.pauses_from_speech: gaps of at least min_pause between and +// around the speech regions, over [0, duration]. +Regions pauses_from_speech(Regions speech, double duration, const Params & p); + +// segment.next_cut: where a segment starting at zero ends. +double next_cut(const Regions & pauses, const Params & p); + +// segment.fits_one_segment for 16 kHz input. +bool fits_one_segment(int64_t n_samples, const Params & p); + +// AudioChunks.chunks(scan_block_seconds, boundary_search_seconds=0): the +// sample count of each scan block, in order. +std::vector scan_block_sizes(int64_t n_samples, const Params & p); + +// Speech source for one scan block: fills `regions` (seconds, relative to +// the block start) for samples [block_start, block_start + block_len). +using SpeechFn = std::function; + +// segment.pause_segments. A clip that fits one segment is returned whole +// without calling `speech`. +transcribe_status pause_segments(int64_t n_samples, + const Params & p, + const SpeechFn & speech, + std::vector & out); + +} // namespace transcribe::parakeet::longform diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp index 85925301a..12463145e 100644 --- a/src/arch/parakeet/model.cpp +++ b/src/arch/parakeet/model.cpp @@ -15,10 +15,12 @@ #include "ggml-backend.h" #include "ggml.h" #include "gguf.h" +#include "longform.h" #include "parakeet.h" #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" #include "transcribe-log.h" @@ -31,6 +33,7 @@ // Metal/Vulkan/CUDA/BLAS at runtime. #include +#include #include #include #include @@ -451,6 +454,12 @@ transcribe_status load(Loader & loader, const transcribe_model_load_params * par return st; } + // A VAD head means transcribe_run segments long audio + // itself (run_longform): advertise the long-form chunker. + if (m->hparams.has_vad_head) { + transcribe::set_feature(m.get(), TRANSCRIBE_FEATURE_LONG_FORM, true); + } + // Derive supports_streaming from hparams: // ChunkedLimited + (L, R) >= 0 — cache-aware streaming // (nemotron-speech-streaming-en-0.6b). @@ -711,6 +720,80 @@ static void normalize_transcript_whitespace(std::string & s) { s.swap(norm); } +// Valid length of each pre_encode stage: [0] = mel frames (n_samples / hop +// under kestrel masking), [1..3] = after each stride-2 conv. +static std::array pre_encode_stage_lengths(const ParakeetHParams & hp, int n_samples, int mel_frames) { + const bool causal = (hp.enc_att_context_style == ParakeetHParams::AttContextStyle::ChunkedLimited); + std::array len{}; + len[0] = hp.kestrel_length_masking ? n_samples / hp.fe_hop_length : mel_frames; + for (int d = 1; d < 4; ++d) { + len[static_cast(d)] = pre_encode_time_out(len[static_cast(d - 1)], causal); + } + return len; +} + +// Fill build_encoder_graph's valid-length masks. Returns each utterance's +// valid encoder length (clamped to T_enc) in real_tenc. +static void fill_length_masks(const EncoderBuild & eb, + const std::vector> & lens, + const std::vector & mel_frames, + int T_enc, + std::vector & real_tenc) { + const int n = static_cast(lens.size()); + real_tenc.assign(static_cast(n), T_enc); + for (int b = 0; b < n; ++b) { + real_tenc[static_cast(b)] = std::min(lens[static_cast(b)][3], T_enc); + } + // Attention key-padding mask [T_enc, 1, 1, n] (0 real / -INF padded) + // and conv valid-frame mask [T_enc, 1, n, 1] (1 real / 0 padded). + if (eb.attn_pad_mask_in != nullptr) { + transcribe::fill_keypad_mask(eb.attn_pad_mask_in, real_tenc, T_enc, n); + } + if (eb.conv_pad_mask_in != nullptr) { + transcribe::fill_valid_frame_mask(eb.conv_pad_mask_in, real_tenc, T_enc, n); + } + // One valid-frame mask per subsampling stage, ne=[1, H_stage, 1, n], + // host index b*H_stage + h. + auto fill_pe_mask = [&](ggml_tensor * mask, int stage) { + if (mask == nullptr) { + return; + } + const int H = static_cast(mask->ne[1]); + std::vector mb(static_cast(H) * n, 0.0f); + for (int b = 0; b < n; ++b) { + const int v = std::min(lens[static_cast(b)][static_cast(stage)], H); + for (int h = 0; h < v; ++h) { + mb[static_cast(b) * H + h] = 1.0f; + } + } + ggml_backend_tensor_set(mask, mb.data(), 0, mb.size() * sizeof(float)); + }; + fill_pe_mask(eb.pre_encode_mask_s1_in, 1); + fill_pe_mask(eb.pre_encode_mask_s2_in, 2); + fill_pe_mask(eb.pre_encode_mask_s3_in, 3); + + // Extent masks (kestrel masking): 1 up to the utterance's own tensor + // length at that stage, i.e. what a single run of it would allocate. + auto fill_extent = [&](ggml_tensor * mask, int stage) { + if (mask == nullptr) { + return; + } + const int H = static_cast(mask->ne[1]); + std::vector mb(static_cast(H) * n, 0.0f); + for (int b = 0; b < n; ++b) { + int t = mel_frames[static_cast(b)]; + for (int d = 0; d < stage; ++d) { + t = pre_encode_time_out(t, /*causal=*/false); + } + std::fill(mb.begin() + static_cast(b) * H, mb.begin() + static_cast(b) * H + std::min(t, H), + 1.0f); + } + ggml_backend_tensor_set(mask, mb.data(), 0, mb.size() * sizeof(float)); + }; + fill_extent(eb.pre_encode_extent_s2_in, 2); + fill_extent(eb.pre_encode_extent_s3_in, 3); +} + // Milliseconds per encoder frame (NeMo: subsampling_factor * hop_length / // sample_rate). Returned as a double — not pre-rounded — so a non-integral // stride doesn't quantize before the call-site multiply. Shared by both @@ -1067,9 +1150,10 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, } } const int mt_keep_frames = static_cast(mt_keep.size()); + const bool kestrel_masks = pm->hparams.kestrel_length_masking; EncoderBuild eb = build_encoder_graph(pc->compute_ctx, pm->weights, pm->hparams, mel_n_frames, resolved_kv, pm->backend.c_str(), /*buf_mask=*/nullptr, /*n_batch=*/1, - /*batch_var_len=*/false, spk_supervision, mt_keep_frames); + /*batch_var_len=*/kestrel_masks, spk_supervision, mt_keep_frames); if (eb.mel_in == nullptr || eb.out == nullptr || eb.graph == nullptr) { return TRANSCRIBE_ERR_GGUF; // build_encoder_graph logged } @@ -1101,6 +1185,14 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, transcribe::debug::dump_tensor("enc.mel.in", eb.mel_in, "encoder.mel"); + int T_valid = -1; + if (kestrel_masks) { + std::vector real_tenc; + fill_length_masks(eb, { pre_encode_stage_lengths(pm->hparams, n_samples, mel_n_frames) }, { mel_n_frames }, + static_cast(eb.out->ne[1]), real_tenc); + T_valid = real_tenc[0]; + } + // Multitalker kernel-mode supervision upload. Length alignment mirrors // NeMo's solve_length_mismatch: pad LEFT with the default value (1.0 // for the target-speaker mask, 0.0 for background) when the @@ -1299,7 +1391,9 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, } transcribe::debug::dump_tensor(p.first.c_str(), p.second, "encoder.block.subblock"); } - try_dump("enc.final", eb.dumps.final_out, "encoder.final"); + if (T_valid < 0) { + try_dump("enc.final", eb.dumps.final_out, "encoder.final"); + } try_dump("enc.prompted", eb.dumps.prompted_out, "encoder.prompted"); pc->encoder_out = eb.out; @@ -1323,6 +1417,21 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, pc->enc_host.resize(static_cast(d_enc) * static_cast(T_enc)); ggml_backend_tensor_get(eb.out, pc->enc_host.data(), 0, pc->enc_host.size() * sizeof(float)); + // kestrel length masking: frames past the valid length are padding (the + // reference masks them out of attention as queries too, so their values + // are unspecified). Decode and dump only the valid rows. + if (T_valid >= 0) { + if (eb.dumps.final_out != nullptr && transcribe::debug::enabled()) { + const long long shape[2] = { T_valid, d_enc }; + transcribe::debug::dump_host_f32("enc.final", pc->enc_host.data(), + static_cast(T_valid) * static_cast(d_enc), shape, 2, + "encoder.final"); + } + const char * enc_dump_name = pm->hparams.has_prompt ? "dec.enc_out_prompted" : nullptr; + return decode_and_populate(pc, pm, params, pc->enc_host.data(), T_valid, d_enc, /*utt_index=*/-1, + enc_dump_name); + } + // Prompt-conditioned models: eb.out is the post-prompt tensor // ("dec.enc_out_prompted"); also read back the pre-prompt final_out // as "dec.enc_out" so both reference dump names match. @@ -1341,6 +1450,310 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, namespace { +// ----- Long-form: VAD-head pause segmentation, per-segment decode ----- + +// GGUF stores the segmenter constants as float32; kestrel's are Python +// float literals. Recover the literal (0.1f -> 0.1) so every float64 +// comparison in the segmenter matches the reference exactly. +double as_decimal(float f) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.6g", static_cast(f)); + return std::strtod(buf, nullptr); +} + +longform::Params longform_params(const ParakeetHParams & hp) { + longform::Params lp; + lp.sample_rate = hp.fe_sample_rate; + // kestrel: 160 / 16_000 * subsampling_factor. + lp.frame_seconds = static_cast(hp.fe_hop_length) / static_cast(hp.fe_sample_rate) * + static_cast(hp.enc_subsampling_factor); + lp.speech_threshold = hp.vad_speech_threshold; + lp.min_speech_seconds = as_decimal(hp.vad_min_speech_seconds); + lp.min_gap_seconds = as_decimal(hp.vad_min_gap_seconds); + lp.max_segment_seconds = as_decimal(hp.seg_max_segment_seconds); + lp.min_segment_seconds = as_decimal(hp.seg_min_segment_seconds); + lp.min_pause_seconds = as_decimal(hp.seg_min_pause_seconds); + lp.scan_block_seconds = as_decimal(hp.seg_scan_block_seconds); + return lp; +} + +void fill_pe_masks(ggml_tensor * s1, ggml_tensor * s2, ggml_tensor * s3, const std::array & len) { + int stage = 1; + for (ggml_tensor * mask : { s1, s2, s3 }) { + if (mask != nullptr) { + const int H = static_cast(mask->ne[1]); + std::vector mb(static_cast(H), 0.0f); + std::fill(mb.begin(), mb.begin() + std::min(len[static_cast(stage)], H), 1.0f); + ggml_backend_tensor_set(mask, mb.data(), 0, mb.size() * sizeof(float)); + } + ++stage; + } +} + +// Speech probability per valid encoder frame of one scan block. +transcribe_status run_vad_block(ParakeetSession * pc, + ParakeetModel * pm, + const float * pcm, + int64_t n_samples, + int block_index, + std::vector & probs) { + std::vector mel; + int n_mels = 0, n_frames = 0; + if (const transcribe_status st = pm->mel->compute(pcm, static_cast(n_samples), mel, n_mels, n_frames); + st != TRANSCRIBE_OK) { + return st; + } + ggml_init_params init_params{}; + init_params.mem_size = 1024 * 1024; + init_params.mem_buffer = nullptr; + init_params.no_alloc = true; + ggml_context * ctx = ggml_init(init_params); + if (ctx == nullptr) { + return TRANSCRIBE_ERR_OOM; + } + + struct CtxGuard { + ggml_context * c; + + ~CtxGuard() { ggml_free(c); } + } guard{ ctx }; + + const VadBuild vb = build_vad_graph(ctx, pm->weights, pm->hparams, n_frames, pm->backend.c_str()); + if (vb.graph == nullptr || vb.prob == nullptr) { + return TRANSCRIBE_ERR_GGUF; + } + if (pc->sched == nullptr) { + pc->sched = ggml_backend_sched_new(pm->plan.scheduler_list.data(), nullptr, + static_cast(pm->plan.scheduler_list.size()), + /*graph_size=*/8192, /*parallel=*/false, /*op_offload=*/true); + if (pc->sched == nullptr) { + return TRANSCRIBE_ERR_BACKEND; + } + } + ggml_backend_sched_reset(pc->sched); + if (!ggml_backend_sched_alloc_graph(pc->sched, vb.graph)) { + return TRANSCRIBE_ERR_OOM; + } + ggml_backend_tensor_set(vb.mel_in, mel.data(), 0, mel.size() * sizeof(float)); + const std::array len = pre_encode_stage_lengths(pm->hparams, static_cast(n_samples), n_frames); + fill_pe_masks(vb.pe_mask_s1_in, vb.pe_mask_s2_in, vb.pe_mask_s3_in, len); + for (ggml_tensor * extent : { vb.pe_extent_s2_in, vb.pe_extent_s3_in }) { + if (extent != nullptr) { // one utterance: its own extent is the whole tensor + const std::vector ones(static_cast(extent->ne[1]), 1.0f); + ggml_backend_tensor_set(extent, ones.data(), 0, ones.size() * sizeof(float)); + } + } + transcribe::configure_sched_n_threads(pc->sched, pc->n_threads); + if (ggml_backend_sched_graph_compute(pc->sched, vb.graph) != GGML_STATUS_SUCCESS) { + return TRANSCRIBE_ERR_BACKEND; + } + + const int T = static_cast(vb.prob->ne[1]); + const int valid = std::min(len[3], T); + std::vector all(static_cast(T)); + ggml_backend_tensor_get(vb.prob, all.data(), 0, all.size() * sizeof(float)); + probs.assign(all.begin(), all.begin() + valid); + + if (transcribe::debug::enabled()) { + const std::string pre = "vad.b" + std::to_string(block_index) + "."; + const long long mshape[] = { n_mels, n_frames }; + transcribe::debug::dump_host_f32((pre + "mel").c_str(), mel.data(), static_cast(mel.size()), mshape, + 2, "vad.mel"); + transcribe::debug::dump_tensor((pre + "pre_encode").c_str(), vb.pre_encode_out, "vad.pre_encode"); + transcribe::debug::dump_tensor((pre + "proj").c_str(), vb.proj_out, "vad.proj"); + transcribe::debug::dump_tensor((pre + "ctx").c_str(), vb.ctx_out, "vad.ctx"); + const long long pshape[] = { valid }; + transcribe::debug::dump_host_f32((pre + "prob").c_str(), probs.data(), valid, pshape, 1, "vad.prob"); + } + return TRANSCRIBE_OK; +} + +// Apply the caller's timestamp ceiling to an assembled multi-segment result +// (build_result_from_raw_tokens does the same for one segment). +void elide_timestamps(ParakeetSession * pc, const ParakeetModel * pm, const transcribe_run_params * params) { + transcribe_timestamp_kind eff = params != nullptr ? params->timestamps : TRANSCRIBE_TIMESTAMPS_AUTO; + if (eff == TRANSCRIBE_TIMESTAMPS_AUTO) { + eff = pm->caps.max_timestamp_kind; + } + if (eff == TRANSCRIBE_TIMESTAMPS_NONE || eff == TRANSCRIBE_TIMESTAMPS_SEGMENT) { + pc->tokens.clear(); + pc->words.clear(); + for (auto & s : pc->segments) { + if (eff == TRANSCRIBE_TIMESTAMPS_NONE) { + s.t0_ms = 0; + s.t1_ms = 0; + } + s.first_word = 0; + s.n_words = 0; + s.first_token = 0; + s.n_tokens = 0; + } + } else if (eff == TRANSCRIBE_TIMESTAMPS_WORD) { + pc->tokens.clear(); + for (auto & w : pc->words) { + w.first_token = 0; + w.n_tokens = 0; + } + for (auto & s : pc->segments) { + s.first_token = 0; + s.n_tokens = 0; + } + } + pc->result_kind = eff; +} + +// Audio longer than one segment: decode each pause segment like a short clip, +// then stitch (texts joined with a space, timestamps offset and clamped). +transcribe_status run_longform(ParakeetSession * pc, + ParakeetModel * pm, + const float * pcm, + int n_samples, + const transcribe_run_params * params) { + transcribe::debug::init(); + const longform::Params lp = longform_params(pm->hparams); + const double rate = static_cast(lp.sample_rate); + + const int64_t t_vad_start = ggml_time_us(); + std::vector all_probs; + int block_index = 0; + const longform::SpeechFn speech = [&](int64_t start, int64_t len, longform::Regions & regions) { + const double duration = static_cast(len) / rate; + regions.clear(); + if (len < lp.min_feature_samples) { + regions.emplace_back(0.0, duration); + return TRANSCRIBE_OK; + } + if (pc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + std::vector probs; + if (const transcribe_status st = run_vad_block(pc, pm, pcm + start, len, block_index++, probs); + st != TRANSCRIBE_OK) { + return st; + } + all_probs.insert(all_probs.end(), probs.begin(), probs.end()); + for (const auto & r : longform::speech_regions(probs.data(), static_cast(probs.size()), lp)) { + regions.emplace_back(r.first, std::min(r.second, duration)); + } + return TRANSCRIBE_OK; + }; + std::vector segments; + if (const transcribe_status st = longform::pause_segments(n_samples, lp, speech, segments); st != TRANSCRIBE_OK) { + return st; + } + const int64_t t_vad_us = ggml_time_us() - t_vad_start; + if (transcribe::debug::enabled() && !all_probs.empty()) { + const long long shape[] = { static_cast(all_probs.size()) }; + transcribe::debug::dump_host_f32("vad.prob", all_probs.data(), static_cast(all_probs.size()), shape, + 1, "vad.prob"); + } + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "parakeet long-form: %d scan blocks, %zu segments", block_index, + segments.size()); + if (transcribe::debug::enabled() && !segments.empty()) { + // (start sample, sample count) per segment; integer-valued f32 is + // exact for validation clips (< 2^24 samples). + std::vector cuts; + for (const auto & sg : segments) { + cuts.push_back(static_cast(sg.start)); + cuts.push_back(static_cast(sg.n_samples)); + } + const long long shape[] = { static_cast(segments.size()), 2 }; + transcribe::debug::dump_host_f32("longform.segments", cuts.data(), static_cast(cuts.size()), shape, + 2, "longform.segments"); + } + + // Decode every segment with full alignment; the caller's timestamp + // ceiling is applied once to the stitched result. + transcribe_run_params seg_params{}; + if (params != nullptr) { + seg_params = *params; + } + seg_params.timestamps = TRANSCRIBE_TIMESTAMPS_TOKEN; + + std::vector tokens; + std::vector words; + std::vector segs; + std::string text, raw; + int64_t t_mel = 0, t_enc = t_vad_us, t_dec = 0; + for (size_t j = 0; j < segments.size(); ++j) { + if (pc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + const longform::Segment & sg = segments[j]; + pc->clear_result(); + const std::string prefix = "seg." + std::to_string(j) + "."; + transcribe::debug::push_name_prefix(prefix.c_str()); + const transcribe_status st = + run_one_shot_inner(pc, pm, pcm + sg.start, static_cast(sg.n_samples), &seg_params, nullptr); + transcribe::debug::pop_name_prefix(); + if (st != TRANSCRIBE_OK) { + return st; + } + t_mel += pc->t_mel_us; + t_enc += pc->t_encode_us; + t_dec += pc->t_decode_us; + + const double offset_ms = 1000.0 * static_cast(sg.start) / rate; + const double end_ms = 1000.0 * static_cast(sg.start + sg.n_samples) / rate; + auto shift = [&](int64_t t) { + return static_cast(std::llround(std::min(static_cast(t) + offset_ms, end_ms))); + }; + const int base_tok = static_cast(tokens.size()); + const int base_word = static_cast(words.size()); + const int seg_index = static_cast(segs.size()); + for (auto tk : pc->tokens) { + tk.t0_ms = shift(tk.t0_ms); + tk.t1_ms = shift(tk.t1_ms); + tk.seg_index = seg_index; + tk.word_index += base_word; + tokens.push_back(std::move(tk)); + } + for (auto wd : pc->words) { + wd.t0_ms = shift(wd.t0_ms); + wd.t1_ms = shift(wd.t1_ms); + wd.seg_index = seg_index; + wd.first_token += base_tok; + words.push_back(std::move(wd)); + } + for (auto se : pc->segments) { + se.t0_ms = shift(se.t0_ms); + se.t1_ms = shift(se.t1_ms); + se.first_word += base_word; + se.first_token += base_tok; + segs.push_back(std::move(se)); + } + if (!pc->full_text.empty()) { + text += (text.empty() ? "" : " ") + pc->full_text; + } + if (!pc->raw_text.empty()) { + raw += (raw.empty() ? "" : " ") + pc->raw_text; + } + } + + pc->clear_result(); + pc->tokens = std::move(tokens); + pc->words = std::move(words); + pc->segments = std::move(segs); + pc->full_text = std::move(text); + pc->raw_text = std::move(raw); + pc->t_mel_us = t_mel; + pc->t_encode_us = t_enc; + pc->t_decode_us = t_dec; + if (!pc->segments.empty()) { + elide_timestamps(pc, pm, params); + pc->has_result = true; + } + return TRANSCRIBE_OK; +} + +bool needs_longform(const ParakeetModel * pm, int n_samples) { + return pm->hparams.has_vad_head && !longform::fits_one_segment(n_samples, longform_params(pm->hparams)); +} + +} // namespace + +namespace { + // One-shot entry point. Validates session/pm, clears the previous result // snapshot, then forwards to the shared inference helper (also reused by // the streaming hooks). @@ -1371,6 +1784,10 @@ transcribe_status run(transcribe_session * session, return run_multitalker(pc, pm, pcm, n_samples, params); } + if (needs_longform(pm, n_samples)) { + return run_longform(pc, pm, pcm, n_samples, params); + } + return run_one_shot_inner(pc, pm, pcm, n_samples, params); } @@ -1389,12 +1806,14 @@ static transcribe_status run_batch_encode(ParakeetSession * ParakeetModel * pm, const std::vector> & mels, const std::vector & nf, + const std::vector & n_samples, int n_mels, int T_max, int64_t total_mel_us, const transcribe_run_params * params) { const int n = static_cast(mels.size()); - bool var_len = false; + // kestrel masking masks every batch: n_samples / hop can trail the mel. + bool var_len = pm->hparams.kestrel_length_masking; for (int b = 0; b < n; ++b) { if (nf[b] != T_max) { var_len = true; @@ -1458,48 +1877,13 @@ static transcribe_status run_batch_encode(ParakeetSession * return TRANSCRIBE_ERR_GGUF; } - // Match each utterance's valid length to the three subsampling convs. - const bool causal_pre_encode = - (pm->hparams.enc_att_context_style == ParakeetHParams::AttContextStyle::ChunkedLimited); std::vector real_tenc(static_cast(n), T_enc); if (var_len) { + std::vector> lens(static_cast(n)); for (int b = 0; b < n; ++b) { - int t = nf[b]; - t = pre_encode_time_out(t, causal_pre_encode); - t = pre_encode_time_out(t, causal_pre_encode); - t = pre_encode_time_out(t, causal_pre_encode); - real_tenc[b] = std::min(t, T_enc); - } - // Attention key-padding mask [T_enc, 1, 1, n] (0 real / -INF padded) - // and conv valid-frame mask [T_enc, 1, n, 1] (1 real / 0 padded). - transcribe::fill_keypad_mask(eb.attn_pad_mask_in, real_tenc, T_enc, n); - transcribe::fill_valid_frame_mask(eb.conv_pad_mask_in, real_tenc, T_enc, n); - - // One valid-frame mask per subsampling ReLU. Each mask is - // ne=[1, H_stage, 1, n], with host index b*H_stage + h. - auto fill_pe_mask = [&](ggml_tensor * mask, int n_down) { - if (mask == nullptr) { - return; - } - const int H = static_cast(mask->ne[1]); - std::vector mb(static_cast(H) * n, 0.0f); - for (int b = 0; b < n; ++b) { - int v = nf[b]; - for (int d = 0; d < n_down; ++d) { - v = pre_encode_time_out(v, causal_pre_encode); - } - if (v > H) { - v = H; - } - for (int h = 0; h < v; ++h) { - mb[static_cast(b) * H + h] = 1.0f; - } - } - ggml_backend_tensor_set(mask, mb.data(), 0, mb.size() * sizeof(float)); - }; - fill_pe_mask(eb.pre_encode_mask_s1_in, 1); // after relu0 - fill_pe_mask(eb.pre_encode_mask_s2_in, 2); // after relu3 - fill_pe_mask(eb.pre_encode_mask_s3_in, 3); // after relu6 + lens[static_cast(b)] = pre_encode_stage_lengths(pm->hparams, n_samples[b], nf[b]); + } + fill_length_masks(eb, lens, nf, T_enc, real_tenc); } // Positional embedding (batch-independent; depends only on T_enc). @@ -1660,6 +2044,32 @@ transcribe_status run_batch(transcribe_session * session, "utterances are decoded single-speaker (use transcribe_run for multitalker)"); } + // Any long-form clip sends the batch through transcribe_run's per-utterance + // paths, so batched and serial results match. + bool any_longform = false; + for (int i = 0; i < n; ++i) { + any_longform = any_longform || (pcm[i] != nullptr && n_samples[i] > 0 && needs_longform(pm, n_samples[i])); + } + if (any_longform) { + for (int i = 0; i < n; ++i) { + if (pc->poll_abort()) { + return TRANSCRIBE_ERR_ABORTED; + } + transcribe_session::ResultSet rs; + if (pcm[i] == nullptr || n_samples[i] <= 0) { + rs.status = TRANSCRIBE_ERR_INVALID_ARG; + pc->batch_results.push_back(std::move(rs)); + continue; + } + pc->clear_result(); + const transcribe_status st = needs_longform(pm, n_samples[i]) ? + run_longform(pc, pm, pcm[i], n_samples[i], params) : + run_one_shot_inner(pc, pm, pcm[i], n_samples[i], params); + pc->batch_results.push_back(pc->capture_result(st)); + } + return TRANSCRIBE_OK; + } + // Compute each utterance's mel in parallel (pure host, no // cross-utterance state). A malformed utterance falls the whole call // back to the per-utterance path (keeps the batch tensor rectangular). @@ -1690,7 +2100,8 @@ transcribe_status run_batch(transcribe_session * session, T_max = std::max(T_max, nf[i]); n_mels = std::max(n_mels, n_mels_per[i]); } - return run_batch_encode(pc, pm, mels, nf, n_mels, T_max, total_mel_us, params); + const std::vector ns(n_samples, n_samples + n); + return run_batch_encode(pc, pm, mels, nf, ns, n_mels, T_max, total_mel_us, params); } // Per-utterance fallback (also the malformed-input path). diff --git a/src/arch/parakeet/weights.cpp b/src/arch/parakeet/weights.cpp index 9a6e083ee..abf4cdb3f 100644 --- a/src/arch/parakeet/weights.cpp +++ b/src/arch/parakeet/weights.cpp @@ -501,6 +501,87 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, ParakeetHPara } } + // kestrel-runtime semantics. Both optional; absent keeps the NeMo behavior. + { + std::string masking; + if (auto st = read_optional_string_kv(gguf, "stt.parakeet.encoder.length_masking", kFamilyTag, "none", masking); + st != TRANSCRIBE_OK) { + return st; + } + if (masking != "none" && masking != "kestrel") { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported encoder.length_masking \"%s\"", masking.c_str()); + return TRANSCRIBE_ERR_GGUF; + } + hp.kestrel_length_masking = (masking == "kestrel"); + + std::string budget; + if (auto st = read_optional_string_kv(gguf, "stt.parakeet.tdt.symbol_budget", kFamilyTag, "per_frame", budget); + st != TRANSCRIBE_OK) { + return st; + } + if (budget != "per_frame" && budget != "global") { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported tdt.symbol_budget \"%s\"", budget.c_str()); + return TRANSCRIBE_ERR_GGUF; + } + hp.tdt_global_symbol_budget = (budget == "global"); + } + + // VAD head + long-form segmenter. Presence of stt.parakeet.vad.hidden is + // the gate; every other key is then required. + if (gguf_find_key(gguf, "stt.parakeet.vad.hidden") >= 0) { + hp.has_vad_head = true; + std::string activation; + + const struct { + const char * key; + float * out; + } f32_keys[] = { + { "stt.parakeet.vad.speech_threshold", &hp.vad_speech_threshold }, + { "stt.parakeet.vad.min_speech_seconds", &hp.vad_min_speech_seconds }, + { "stt.parakeet.vad.min_gap_seconds", &hp.vad_min_gap_seconds }, + { "stt.parakeet.segmenter.max_segment_seconds", &hp.seg_max_segment_seconds }, + { "stt.parakeet.segmenter.min_segment_seconds", &hp.seg_min_segment_seconds }, + { "stt.parakeet.segmenter.min_pause_seconds", &hp.seg_min_pause_seconds }, + { "stt.parakeet.segmenter.scan_block_seconds", &hp.seg_scan_block_seconds }, + }; + + if (auto st = read_required_u32_kv(gguf, "stt.parakeet.vad.hidden", kFamilyTag, hp.vad_hidden); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_u32_kv(gguf, "stt.parakeet.vad.context_kernel", kFamilyTag, hp.vad_context_kernel); + st != TRANSCRIBE_OK) { + return st; + } + if (auto st = read_required_string_kv(gguf, "stt.parakeet.vad.activation", kFamilyTag, activation); + st != TRANSCRIBE_OK) { + return st; + } + for (const auto & k : f32_keys) { + if (auto st = read_required_f32_kv(gguf, k.key, kFamilyTag, *k.out); st != TRANSCRIBE_OK) { + return st; + } + } + if (activation != "silu" || hp.vad_hidden <= 0 || hp.vad_context_kernel <= 0 || + (hp.vad_context_kernel % 2) == 0 || hp.seg_max_segment_seconds <= hp.seg_min_segment_seconds || + hp.seg_scan_block_seconds < hp.seg_max_segment_seconds) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: unsupported vad/segmenter config (activation \"%s\", hidden %d, kernel %d)", + activation.c_str(), hp.vad_hidden, hp.vad_context_kernel); + return TRANSCRIBE_ERR_GGUF; + } + } + + if ((hp.kestrel_length_masking || hp.has_vad_head) && + hp.enc_att_context_style != ParakeetHParams::AttContextStyle::Regular) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: kestrel masking / VAD head require regular attention"); + return TRANSCRIBE_ERR_GGUF; + } + if (hp.tdt_global_symbol_budget && hp.head_kind != HeadKind::TDT) { + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: tdt.symbol_budget=global requires a TDT head"); + return TRANSCRIBE_ERR_GGUF; + } + // Speaker-kernel metadata is optional; absent means no injection. if (gguf_find_key(gguf, "stt.parakeet.encoder.spk_kernel_layers") >= 0) { switch (read_int32_array_kv(gguf, "stt.parakeet.encoder.spk_kernel_layers", hp.spk_kernel_layers)) { @@ -938,6 +1019,18 @@ transcribe_status build_parakeet_weights(ggml_context * ctx_meta, } } + // ----- optional VAD head ----- + if (hp.has_vad_head) { + const int h = hp.vad_hidden; + const int kc = hp.vad_context_kernel; + GET_CONV(weights.vad.proj_w, "vad.proj.weight", 1, d_model, h); + GET_F32(weights.vad.proj_b, "vad.proj.bias", h); + GET_CONV(weights.vad.ctx_w, "vad.ctx.weight", kc, h, h); + GET_F32(weights.vad.ctx_b, "vad.ctx.bias", h); + GET_CONV(weights.vad.out_w, "vad.out.weight", 1, h, 1); + GET_F32(weights.vad.out_b, "vad.out.bias", 1); + } + return TRANSCRIBE_OK; } diff --git a/src/arch/parakeet/weights.h b/src/arch/parakeet/weights.h index 6ab4f4cb6..c81ebff49 100644 --- a/src/arch/parakeet/weights.h +++ b/src/arch/parakeet/weights.h @@ -168,6 +168,33 @@ struct ParakeetHParams { // frame, forces a +1 advance. 0 disables. int32_t tdt_max_symbols = 10; + // tdt_global_symbol_budget: kestrel's greedy loop. No per-frame cap; + // every decode step, blank or not, spends from one budget of + // tdt_max_symbols * T_valid, and a blank with duration 0 advances one + // frame. KV stt.parakeet.tdt.symbol_budget = "global"; absent / + // "per_frame" keeps the NeMo loop above. + bool tdt_global_symbol_budget = false; + + // kestrel_length_masking: valid lengths from n_samples / hop, masks at + // every batch size (see ConvPolicy::pre_encode_mask_after_stride). KV + // stt.parakeet.encoder.length_masking = "kestrel"; absent / "none" keeps + // the NeMo behavior (masks only in variable-length batches). + bool kestrel_length_masking = false; + + // Speech head on the subsampler output and the long-form pause + // segmenter it drives. has_vad_head is set when stt.parakeet.vad.hidden + // is present. + bool has_vad_head = false; + int32_t vad_hidden = 0; + int32_t vad_context_kernel = 0; + float vad_speech_threshold = 0.5f; + float vad_min_speech_seconds = 0.1f; + float vad_min_gap_seconds = 0.1f; + float seg_max_segment_seconds = 30.0f; + float seg_min_segment_seconds = 1.0f; + float seg_min_pause_seconds = 0.2f; + float seg_scan_block_seconds = 120.0f; + // Frontend (mel feature extractor). The complete stt.frontend.* set // the converter emits and the C++ frontend reads; the loader gates on // it. CMVN/LFR fields are omitted (no published Parakeet variant uses @@ -373,6 +400,17 @@ struct ParakeetSpkKernel { ParakeetSpkKernelFF bg; // enc.bg_spk_kernel..* (add_bg_spk_kernel) }; +// Speech head. PyTorch Conv1d [out, in, k] -> ggml ne +// [k, in, out]: proj [1, d_model, H], ctx [K, H, H], out [1, H, 1]. +struct ParakeetVadHead { + ggml_tensor * proj_w = nullptr; + ggml_tensor * proj_b = nullptr; // [H] + ggml_tensor * ctx_w = nullptr; + ggml_tensor * ctx_b = nullptr; // [H] + ggml_tensor * out_w = nullptr; + ggml_tensor * out_b = nullptr; // [1] +}; + struct ParakeetWeights { ParakeetPreEncode pre_encode; std::vector blocks; // hp.enc_n_layers entries @@ -381,6 +419,7 @@ struct ParakeetWeights { ParakeetCtcHead ctc_head; // populated when head_kind=CTC ParakeetPromptMlp prompt; // populated when hp.has_prompt std::vector spk_kernels; // populated when hp.has_spk_kernel + ParakeetVadHead vad; // populated when hp.has_vad_head }; // Walk the canonical tensor list, look up each tensor by name, validate diff --git a/src/conformer/conformer.cpp b/src/conformer/conformer.cpp index f811d36d6..4ae01a8cc 100644 --- a/src/conformer/conformer.cpp +++ b/src/conformer/conformer.cpp @@ -112,6 +112,18 @@ ggml_tensor * rel_shift(ggml_context * ctx, ggml_tensor * x) { return y; } +// f32 Conv2D (see conformer.h). +ggml_tensor * +conv_2d_f32(ggml_context * ctx, ggml_tensor * a, ggml_tensor * b, int s0, int s1, int p0, int p1, int d0, int d1) { + ggml_tensor * im2col = ggml_im2col(ctx, a, b, s0, s1, p0, p1, d0, d1, /*is_2D=*/true, + GGML_TYPE_F32); // [N, OH, OW, IC * KH * KW] + ggml_tensor * result = + ggml_mul_mat(ctx, ggml_reshape_2d(ctx, im2col, im2col->ne[0], im2col->ne[3] * im2col->ne[2] * im2col->ne[1]), + ggml_reshape_2d(ctx, a, a->ne[0] * a->ne[1] * a->ne[2], a->ne[3])); + result = ggml_reshape_4d(ctx, result, im2col->ne[1], im2col->ne[2], im2col->ne[3], a->ne[3]); // [OC, N, OH, OW] + return ggml_cont(ctx, ggml_permute(ctx, result, 0, 1, 3, 2)); // [N, OC, OH, OW] +} + // f32-friendly Conv1D (mirrors ggml_conv_1d but passes the kernel's real // type to im2col instead of forcing f16). Vendored ggml's ggml_conv_1d // hardcodes GGML_TYPE_F16 for the im2col output and asserts on an f32 kernel. @@ -1074,19 +1086,29 @@ ggml_tensor * build_pre_encode(ggml_context * ctx, /*p0=*/pe_p_op, /*p1=*/pe_p_op, /*d0=*/1, /*d1=*/1); } - x = add_conv_bias(ctx, x, pe.conv2_b); - x = name_prefixed(x, name_prefix, "conv2"); + x = add_conv_bias(ctx, x, pe.conv2_b); + x = name_prefixed(x, name_prefix, "conv2"); + const bool after_stride = policy.pre_encode_mask_after_stride; + if (after_stride) { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s2 : nullptr, "pre_encode.valid_mask.s2"); + } // conv3 (pointwise: channels -> channels, k=1 s=1 p=0) - x = ggml_conv_2d(ctx, pe.conv3_w, x, - /*s0=*/1, /*s1=*/1, - /*p0=*/0, /*p1=*/0, - /*d0=*/1, /*d1=*/1); + x = policy.pre_encode_f32_pointwise ? + conv_2d_f32(ctx, pe.conv3_w, x, /*s0=*/1, /*s1=*/1, /*p0=*/0, /*p1=*/0, /*d0=*/1, /*d1=*/1) : + ggml_conv_2d(ctx, pe.conv3_w, x, + /*s0=*/1, /*s1=*/1, + /*p0=*/0, /*p1=*/0, + /*d0=*/1, /*d1=*/1); x = add_conv_bias(ctx, x, pe.conv3_b); x = name_prefixed(x, name_prefix, "conv3"); x = ggml_relu(ctx, x); x = name_prefixed(x, name_prefix, "relu3"); - x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s2 : nullptr, "pre_encode.valid_mask.s2"); + if (!after_stride) { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s2 : nullptr, "pre_encode.valid_mask.s2"); + } else { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s2_extent : nullptr, "pre_encode.extent_mask.s2"); + } // conv5 (depthwise) -> conv6 (pointwise) -> ReLU x = pad_causal(x); @@ -1103,16 +1125,25 @@ ggml_tensor * build_pre_encode(ggml_context * ctx, } x = add_conv_bias(ctx, x, pe.conv5_b); x = name_prefixed(x, name_prefix, "conv5"); + if (after_stride) { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s3 : nullptr, "pre_encode.valid_mask.s3"); + } - x = ggml_conv_2d(ctx, pe.conv6_w, x, - /*s0=*/1, /*s1=*/1, - /*p0=*/0, /*p1=*/0, - /*d0=*/1, /*d1=*/1); + x = policy.pre_encode_f32_pointwise ? + conv_2d_f32(ctx, pe.conv6_w, x, /*s0=*/1, /*s1=*/1, /*p0=*/0, /*p1=*/0, /*d0=*/1, /*d1=*/1) : + ggml_conv_2d(ctx, pe.conv6_w, x, + /*s0=*/1, /*s1=*/1, + /*p0=*/0, /*p1=*/0, + /*d0=*/1, /*d1=*/1); x = add_conv_bias(ctx, x, pe.conv6_b); x = name_prefixed(x, name_prefix, "conv6"); x = ggml_relu(ctx, x); x = name_prefixed(x, name_prefix, "relu6"); - x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s3 : nullptr, "pre_encode.valid_mask.s3"); + if (!after_stride) { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s3 : nullptr, "pre_encode.valid_mask.s3"); + } else { + x = apply_valid_mask(x, valid_masks ? &valid_masks->mask_s3_extent : nullptr, "pre_encode.extent_mask.s3"); + } // At this point ne = [F'=16, T_enc, channels=256, 1] where // T_enc = floor(T_mel / 8). Flatten (F', C) into one feature axis diff --git a/src/conformer/conformer.h b/src/conformer/conformer.h index 7ed7af9d4..ff296bafc 100644 --- a/src/conformer/conformer.h +++ b/src/conformer/conformer.h @@ -119,6 +119,16 @@ struct ConvPolicy { // path, and shifts both the freq and time output dims. False on every // offline variant; true on nemotron-speech-streaming-en. bool causal_pre_encode = false; + + // Where the pre_encode valid-length masks apply. false (NeMo): after each + // stage's ReLU. true (kestrel / HF Subsampling): right after each strided + // conv, so the padded tail carries ReLU(pointwise bias) into the next + // strided conv as the reference does. Stage 1 is the same either way. + bool pre_encode_mask_after_stride = false; + + // Run the pre_encode pointwise convs (conv3, conv6) via conv_2d_f32. + // Other variants were validated on the F16 im2col and keep it. + bool pre_encode_f32_pointwise = false; }; // Resolve a conv-dispatch toggle from its env overrides. The DIRECT var forces @@ -284,6 +294,11 @@ ggml_tensor * macaron_ff_residual(ggml_context * ctx, // matrix rotated so column k holds the score for relative offset k. ggml_tensor * rel_shift(ggml_context * ctx, ggml_tensor * x); +// ggml_conv_2d with an F32 im2col. The vendored op rounds the activations to +// F16 in im2col unless the kernel is BF16 (~5e-4 rel on parakeet pre_encode). +ggml_tensor * +conv_2d_f32(ggml_context * ctx, ggml_tensor * a, ggml_tensor * b, int s0, int s1, int p0, int p1, int d0, int d1); + // f32-friendly Conv1D. Use instead of ggml_conv_1d for fp32 kernels on // Metal (see conv_2d_dw_f32 in conformer.cpp). ggml_tensor * conv_1d_f32(ggml_context * ctx, @@ -426,9 +441,14 @@ ggml_tensor * build_conformer_block(ggml_context * ctx, // (one per ReLU stage), applies them, and writes the handles back for the // driver to fill. Offline (non-causal) pre_encode only. struct PreEncodeValidMasks { - ggml_tensor * mask_s1 = nullptr; // after relu0 - ggml_tensor * mask_s2 = nullptr; // after relu3 - ggml_tensor * mask_s3 = nullptr; // after relu6 + ggml_tensor * mask_s1 = nullptr; // after relu0 + ggml_tensor * mask_s2 = nullptr; // after relu3 (after conv2 with pre_encode_mask_after_stride) + ggml_tensor * mask_s3 = nullptr; // after relu6 (after conv5 with pre_encode_mask_after_stride) + // pre_encode_mask_after_stride only: zero positions past each utterance's + // single-run extent after relu3 / relu6, so a padded batch stays + // bit-identical to single runs (which never see the ReLU(bias) tail). + ggml_tensor * mask_s2_extent = nullptr; + ggml_tensor * mask_s3_extent = nullptr; }; // DwStridingSubsampling pre_encode stack. Returns the final diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3ec7871f9..5783bda99 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -193,6 +193,13 @@ transcribe_apply_warnings(transcribe_parakeet_chunked_limited_with_rc_mask_unit) add_test(NAME transcribe_parakeet_chunked_limited_with_rc_mask_unit COMMAND transcribe_parakeet_chunked_limited_with_rc_mask_unit) +# Long-form pause segmenter vs kestrel-generated expected cut points. +add_executable(transcribe_parakeet_longform_unit parakeet_longform_unit.cpp) +target_link_libraries(transcribe_parakeet_longform_unit PRIVATE transcribe ggml) +target_include_directories(transcribe_parakeet_longform_unit PRIVATE ${CMAKE_SOURCE_DIR}/src) +transcribe_apply_warnings(transcribe_parakeet_longform_unit) +add_test(NAME transcribe_parakeet_longform_unit COMMAND transcribe_parakeet_longform_unit) + # ----------------------------------------------------------------------------- # Variable-length batch mask and causal subsampling unit test # ----------------------------------------------------------------------------- diff --git a/tests/golden/batch/parakeet-ultra.cpu.json b/tests/golden/batch/parakeet-ultra.cpu.json new file mode 100644 index 000000000..f4674d791 --- /dev/null +++ b/tests/golden/batch/parakeet-ultra.cpu.json @@ -0,0 +1,20 @@ +{ + "model": "models/parakeet-ultra/parakeet-ultra-F32.gguf", + "backend": "cpu", + "language": null, + "texts": { + "samples/jfk.wav": "And so, my fellow Americans, ask not what your country can do for you, ask what you can do for your country.", + "samples/german.wav": "Am Strand der Badeanzug, die Badehose, die Sandalen, die Luftmatratze, das Handtuch, das Eis, der Ball, die Sonne, das Meer, die Wellen.", + "samples/wer/fleurs-es/fleurs-es-1001146817223348054.wav": "Se recomienda enfáticamente a los viajeros que se informen sobre cualquier riesgo del clima extremo en el área que visitan, dado que ello pueda afectar sus planes de viaje.", + "samples/wer/librispeech-test-clean/1089-134686-0000.wav": "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 fattened sauce.", + "samples/wer/librispeech-test-clean/1089-134686-0001.wav": "Stuff it into you, his belly counseled him.", + "samples/wer/librispeech-test-clean/1089-134686-0002.wav": "After early nightfall the yellow lamps would light up here and there the squalid quarter of the brothels.", + "samples/wer/librispeech-test-clean/1089-134686-0003.wav": "Hello, Bertie, any good in your mind?", + "samples/wer/librispeech-test-clean/1089-134686-0004.wav": "Number ten Fresh Nelly is waiting on you. Good night, husband.", + "samples/dots.wav": "Of course, it was impossible to connect the dots looking forward when I was in college, but it was very, very clear looking backwards ten years later. Again, you can't connect the dots looking forward. You can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future. You have to trust in something, your gut, destiny, life, karma, whatever, because believing that the dots will connect down the road will give you the confidence to follow your heart. even when it leads you off the well-worn path, and that will make all the difference.", + "samples/product-names.wav": "Welcome to Quirk Quid Quill Inc., where finance meets innovation. Explore diverse offerings from the P3 Quattro, a unique investment portfolio quadrant, to the O3 Omni, a platform for intricate derivative trading strategies. Delve into unconventional bond markets with our B3 Bond X and our and experience non-standard equity trading with E3 equity. Personalize your wealth management with W3 RAPZ and anticipate market trends with the O2 Outlier, our forward-thinking financial forecasting tool. Explore venture capital world with U3 Unifund or move your money with the M3 Mover, our sophisticated monetary transfer module. At QuarkQuid Quill Inc., we turn complex finance into creative solutions. Join us in redefining financial services.", + "samples/cj-swimming-drop.wav": "Yeah, more swimming-related vocab would be helpful. The thing is that like mostly I notice in people typically is them like dropping their hips and probably needing to press their chest a bit. And most of these people are triathletes and I think they think that like their legs just sink, but really they just don't know how to use their core to elevate their butt and which brings the legs up. I think most people are able to swim basically without kicking if they have good body position. And that's like the majority of learning how to swim is like learning how to put your body in the right position because from there you can be very efficient, I feel like. So Things around this seem like it makes sense. And then there's like certain drills like catch-up freestyle or fingertip drag that are quite useful in terms of elongating someone's stroke and also probably learning words around drills and like a pull buoy kind of a thing. Yeah.", + "samples/whole-earth.wav": "When I was young, there was an amazing publication called The Whole Earth Catalog, which was one of the Bibles of my generation. It was created by a fellow named Stuart Brand, not far from here in Menlo Park, and he brought it to life with his poetic touch. This was in the late 60s, before personal computers and desktop publishing, so it was all made with typewriters, scissors, and Polaroid cameras. It was sort of like Google in paperback form 35 years before Google came along. It was idealistic, overflowing with neat tools and great notions. Stewart and his team put out several issues of the Whole Earth catalog, and then, when it had run its course, they put out a final issue. It was the mid-1970s, and I was your age. On the back cover of their final issue was a photograph of an early morning country road. the kind you might find yourself hitchhiking on if you were so adventurous. Beneath it were the words stay hungry, stay foolish. It was their farewell message as they signed off stay hungry, stay foolish. And I have always wished that for myself. And now, as you graduate to begin anew, I wish that for you. Stay hungry, stay foolish. Thank you all very much.", + "samples/death.wav": "My third story is about death. When I was 17, I read a quote that went something like, If you live each day as if it was your last, someday you'll most certainly be right. It made an impression on me, and since then, for the past 33 years, I have looked in the mirror every morning and asked myself if today were the last day of my life would I want to do what I am about to do today. And whenever the answer has been no for too many days in a row, I know I need to change something. Remembering that I'll be dead soon is the most important tool I've ever encountered to help me make the big choices in life. Because almost everything, all external expectations, all pride, all fear of embarrassment or failure, these things just fall away in the face of death, leaving only what is truly important. Remembering that you are going to die is the best way I know to avoid the trap of thinking you have something to lose. You are already naked. There is no reason not to follow your heart. About a year ago, I was diagnosed with cancer. I had a scan at 7 30 in the morning and it clearly showed a tumor on my pancreas. I didn't even know what a pancreas was. The doctors told me this was almost certainly a type of cancer that is incurable and that I should expect to live no longer than three to six months. My doctor advised me to go home and get my affairs in order, which is doctor's code for prepare to die. It means to try and tell your kids everything you thought you'd have the next ten years to tell them in just a few months. It means to make sure everything is buttoned up. so that it will be as easy as possible for your family. It means to say your goodbyes. I live with that diagnosis all day. Later that evening I had a biopsy where they stuck an endoscope down my throat, through my stomach and into my intestines, put a needle into my pancreas and got a few cells from the tumor. I was sedated, but my wife, who was there, told me that when they viewed the cells under a microscope, the doctors started crying. because it turned out to be a very rare form of pancreatic cancer that is curable with surgery. I had the surgery and thankfully I'm fine now. This was the closest I've been to facing death, and I hope it's the closest I get for a few more decades. Having lived through it, I can now say this to you with a bit more certainty than when death was a useful but purely intellectual concept. No one wants to die. Even people who want to go to heaven don't want to die to get there. And yet, death is the destination we all share. No one has ever escaped it, and that is as it should be, because death is very likely the single best invention of life. It's life's change agent. It clears out the old to make way for the new. Right now, the new is you. But someday, not too long from now, you will gradually become the old and be cleared away. Sorry to be so dramatic, but it's quite true. Your time is limited, so don't waste it living someone else's life. Don't be trapped by dogma, which is living with the results of other people's thinking. Don't let the noise of others' opinions drown out your own inner voice. And most important, have the courage to follow your heart and intuition. They somehow already know what you truly want to become. Everything else is secondary." + } +} diff --git a/tests/golden/parakeet/parakeet-ultra.manifest.json b/tests/golden/parakeet/parakeet-ultra.manifest.json new file mode 100644 index 000000000..cafef7624 --- /dev/null +++ b/tests/golden/parakeet/parakeet-ultra.manifest.json @@ -0,0 +1,107 @@ +{ + "schema": "transcribe-golden-manifest-v1", + "family": "parakeet", + "variant": "parakeet-ultra", + "source_model": { + "hf_repo": "moondream/parakeet-ultra", + "hf_revision": "73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b" + }, + "expected_dtype": "float32", + "dtype_source": "config", + "reference": { + "kind": "author_repo_kestrel", + "source": "https://pypi.org/project/kestrel/", + "revision": "kestrel==0.9.1, kestrel-kernels==0.7.4", + "entrypoint": "scripts/dump_reference_parakeet_kestrel.py", + "env": "parakeet-kestrel", + "dump_args": [ + "--revision", + "73175eb7aeb0d82f1e2a6b53b3aabc10a90bcd0b" + ] + }, + "frontend": { + "sample_rate": 16000, + "n_mels": 128, + "hop_length": 160, + "fft_size": 512, + "window": "hann_symmetric", + "normalization": "per_feature", + "preemphasis": 0.97, + "dither": 0.0, + "center": true, + "padding_mode": "constant", + "mel_filterbank_norm": "slaney", + "win_length": 400 + }, + "tokenizer_summary": { + "type": "sentencepiece", + "vocab_size": 8193, + "special_tokens": { + "pad": 2, + "blank": 8192, + "decoder_start": 8192 + } + }, + "capabilities": { + "languages": [ + "en", + "de", + "fr", + "es", + "it", + "pt", + "ru", + "uk", + "hr", + "sl", + "lv", + "lt", + "et", + "fi", + "sv", + "da", + "nl", + "pl", + "cs", + "sk", + "hu", + "ro", + "bg", + "el", + "mt" + ], + "language_detection": true, + "translation": false, + "timestamps": [ + "token", + "word", + "segment" + ], + "streaming": false, + "speaker_diarization": false + }, + "tolerance_file": "tests/tolerances/parakeet-ultra.json", + "cases": [ + { + "audio": "jfk", + "language": null + }, + { + "audio": "dots", + "language": null, + "stages": [ + "longform" + ] + }, + { + "audio": "dots-full", + "language": null, + "stages": [ + "longform" + ] + } + ], + "cpp_env": { + "TRANSCRIBE_NO_FLASH": "1" + } +} diff --git a/tests/parakeet_longform_unit.cpp b/tests/parakeet_longform_unit.cpp new file mode 100644 index 000000000..23ac93d5f --- /dev/null +++ b/tests/parakeet_longform_unit.cpp @@ -0,0 +1,230 @@ +// Unit test for the parakeet long-form segmenter. Expected values come from +// kestrel run on the synthetic inputs below; the port must match exactly. + +#include "arch/parakeet/longform.h" + +#include +#include +#include +#include + +namespace lf = transcribe::parakeet::longform; + +namespace { + +const float kProbs[] = { + 0.4500615f, 0.541782081f, 0.589053869f, 0.632300556f, 0.725424111f, 0.766298711f, 0.882188976f, + 1.0f, 0.964906871f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 0.997145414f, 0.993273318f, 0.923860729f, + 0.955549121f, 0.849034488f, 0.865483701f, 0.794413388f, 0.707816124f, 0.518682182f, 0.542591631f, + 0.490662545f, 0.42179355f, 0.263071239f, 0.240475029f, 0.142465368f, 0.0811041072f, 0.108715832f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0046699564f, 0.200000003f, 0.200000003f, 0.0800082237f, 0.172352418f, 0.205019042f, 0.322391063f, + 0.360662341f, 0.474132925f, 0.589590609f, 0.559957325f, 0.678235888f, 0.716615975f, 0.814344347f, + 0.812493563f, 0.901661336f, 0.973779976f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 0.960966825f, 0.995472193f, 0.838040769f, 0.779455125f, 0.743299723f, 0.647330642f, + 0.568745613f, 0.44659245f, 0.424773604f, 0.32652244f, 0.331584215f, 0.232644737f, 0.128517687f, + 0.0967499092f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0582143404f, 0.170369923f, 0.231782749f, + 0.227596164f, 0.350877106f, 0.433307052f, 0.455748111f, 0.597687185f, 0.616392076f, 0.77995801f, + 0.809655845f, 0.5f, 0.49999997f, 0.971639037f, 0.925000072f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.989855468f, 1.0f, 1.0f, 0.9228127f, 0.853196561f, 0.794649124f, 0.685372293f, + 0.717112184f, 0.560526371f, 0.508935392f, 0.394910723f, 0.32653895f, 0.218337402f, 0.271486223f, + 0.130255073f, 0.119354121f, 0.00940343179f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.163776666f, + 0.195986852f, 0.273916125f, 0.308085114f, 0.445057273f, 0.472305208f, 0.568481386f, 0.595051348f, + 0.66207397f, 0.85877192f, 0.831622362f, 0.931624353f, 0.969768107f, 0.997514367f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 0.925792038f, 0.901364446f, 0.849656701f, + 0.7840994f, 0.717028797f, 0.573927283f, 0.596431971f, 0.381474435f, 0.410085887f, 0.315895259f, + 0.26099813f, 0.240198702f, 0.15307337f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0784521028f, + 0.115800112f, 0.208901092f, 0.211974606f, 0.30368039f, 0.363146096f, 0.44572714f, 0.576306403f, + 0.602455378f, 0.732159436f, 0.800900459f, 0.950690985f, 0.840485454f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 0.965194583f, 1.0f, 0.893005252f, 0.776896119f, 0.855068624f, + 0.885141909f, 0.753255546f, 0.621070743f, 0.558684886f, 0.586390138f, 0.460915029f, 0.378571242f, + 0.297528207f, 0.227962047f, 0.194860205f, 0.114337049f, 0.0340040736f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0497603379f, 0.0569235757f, 0.166568398f, 0.243654519f, 0.386890411f, 0.420606196f, 0.4805637f, + 0.633873701f, 0.604948103f, 0.686315656f, 0.684934199f, 0.920274138f, 0.951346159f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.949401021f, + 0.871494353f, 0.770826578f, 0.755863607f, 0.732079327f, 0.595077693f, 0.527659595f, 0.451211095f, + 0.390835404f, 0.229579404f, 0.223035902f, 0.120263144f, 0.138858676f, 0.0f, 0.000436047936f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.042441126f, 0.0f, 0.156791449f, 0.247761667f, 0.256696194f, 0.346078932f, 0.492292017f, + 0.461781859f, 0.518645465f, 0.556123555f, 0.699259102f, 0.841531754f, 0.882116437f, 0.848950803f, + 0.909813464f, 0.977825403f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.868186772f, 0.915354133f, 0.73669523f, 0.697008312f, 0.739785373f, 0.658972323f, 0.540671527f, + 0.385989219f, 0.375983864f, 0.284346163f, 0.275449067f, 0.284312338f, 0.113458477f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0703902617f, 0.0851784274f, 0.173921421f, 0.285013676f, 0.222699255f, + 0.346277177f +}; + +const std::vector> kSpeechRegions = { + { 0.08, 2.24 }, + { 4.64, 6.8 }, + { 9.28, 11.44 }, + { 13.84, 16.0 }, + { 18.400000000000002, 20.56 }, + { 22.96, 25.12 }, + { 27.52, 29.68 } +}; + +// Absolute pauses (seconds) driving the synthetic speech source. +const std::vector> kPauses = { + { 11.64, 11.72 }, + { 23.35, 23.51 }, + { 33.94, 34.84 }, + { 48.25, 48.65 }, + { 55.85, 56.01 }, + { 66.31, 66.55 }, + { 78.57, 78.97 }, + { 89.01, 89.17 }, + { 93.68, 93.76 }, + { 104.99, 105.23 }, + { 108.01, 108.41 }, + { 110.6, 111.0 }, + { 124.5, 124.9 }, + { 131.81, 132.05 }, + { 142.69, 142.93 }, + { 153.7, 153.94 }, + { 156.96, 157.86 }, + { 166.56, 166.8 }, + { 179.98, 180.38 }, + { 187.82, 187.9 }, + { 197.47, 197.55 }, + { 200.44, 200.68 }, + { 209.8, 209.96 }, + { 214.3, 214.46 }, + { 227.01, 227.25 }, + { 234.7, 234.78 }, + { 245.78, 246.68 }, + { 255.32, 255.72 }, + { 267.41, 267.49 }, + { 276.93, 277.17 }, + { 289.0, 289.16 }, + { 298.86, 299.26 }, + { 306.14, 306.22 }, + { 312.97, 313.21 }, + { 324.14, 325.04 }, + { 332.64, 332.8 }, + { 343.87, 344.77 }, + { 350.82, 351.06 }, + { 362.98, 363.88 }, + { 376.02, 376.18 }, + { 387.6, 388.5 }, + { 399.06, 399.3 }, + { 401.71, 402.11 } +}; + +struct Case { + int64_t n_samples; + std::vector blocks; + std::vector> segments; // (start, n_samples) +}; + +const Case kCases[] = { + { 4800123, + { 1920000, 1920000, 960123 }, + { { 0, 480000 }, + { 480000, 295200 }, + { 775200, 287680 }, + { 1062880, 197440 }, + { 1260320, 471040 }, + { 1731360, 379520 }, + { 2110880, 407680 }, + { 2518560, 364320 }, + { 2882880, 326080 }, + { 3208960, 425120 }, + { 3634080, 454240 }, + { 4088320, 344480 }, + { 4432800, 367323 } } }, + { 1925000, + { 1917000, 8000 }, + { { 0, 480000 }, + { 480000, 295200 }, + { 775200, 287680 }, + { 1062880, 197440 }, + { 1260320, 471040 }, + { 1731360, 193640 } } }, + { 480001, { 480001 }, { { 0, 480000 } } }, +}; + +int g_failures = 0; + +void check(bool ok, const char * what) { + if (!ok) { + std::fprintf(stderr, "FAIL: %s\n", what); + ++g_failures; + } +} + +} // namespace + +int main() { + const lf::Params p; // kestrel defaults: 16 kHz, 0.08 s frames, 0.5/0.1/0.1, 30/1/0.2 s, 120 s blocks + + // vad.speech_regions + const lf::Regions got = lf::speech_regions(kProbs, static_cast(sizeof(kProbs) / sizeof(kProbs[0])), p); + check(got == lf::Regions(kSpeechRegions.begin(), kSpeechRegions.end()), "speech_regions matches kestrel"); + + // fits_one_segment / round_half_even + check(lf::fits_one_segment(480000, p) && !lf::fits_one_segment(480001, p), "fits_one_segment boundary at 30 s"); + check(lf::round_half_even(2.5) == 2 && lf::round_half_even(3.5) == 4 && lf::round_half_even(-0.5) == 0, + "round_half_even ties to even"); + + for (const Case & c : kCases) { + char label[96]; + std::snprintf(label, sizeof(label), "scan_block_sizes(%lld)", static_cast(c.n_samples)); + check(lf::scan_block_sizes(c.n_samples, p) == c.blocks, label); + + // The same synthetic source the fixture used: speech everywhere except + // the absolute pauses, reported per block relative to its start. + const lf::SpeechFn speech = [&](int64_t start_sample, int64_t len, lf::Regions & regions) { + const double start = static_cast(start_sample) / p.sample_rate; + const double dur = static_cast(len) / p.sample_rate; + double cur = start; + regions.clear(); + for (const auto & pz : kPauses) { + if (pz.second <= start || pz.first >= start + dur) { + continue; + } + if (pz.first > cur) { + regions.emplace_back(cur - start, pz.first - start); + } + cur = std::max(cur, pz.second); + } + if (cur < start + dur) { + regions.emplace_back(cur - start, dur); + } + return TRANSCRIBE_OK; + }; + std::vector segs; + check(lf::pause_segments(c.n_samples, p, speech, segs) == TRANSCRIBE_OK, "pause_segments status"); + bool same = segs.size() == c.segments.size(); + for (size_t i = 0; same && i < segs.size(); ++i) { + same = segs[i].start == c.segments[i].first && segs[i].n_samples == c.segments[i].second; + } + std::snprintf(label, sizeof(label), "pause_segments(%lld) matches kestrel", + static_cast(c.n_samples)); + check(same, label); + } + + if (g_failures == 0) { + std::printf("parakeet longform unit: OK\n"); + } + return g_failures == 0 ? 0 : 1; +} diff --git a/tests/tolerances/parakeet-ultra.json b/tests/tolerances/parakeet-ultra.json new file mode 100644 index 000000000..105e81a9f --- /dev/null +++ b/tests/tolerances/parakeet-ultra.json @@ -0,0 +1,260 @@ +{ + "_comment": [ + "parakeet-ultra per-tensor tolerances for compare_tensors.py (finalized Stage 4, 2026-10-04).", + "", + "CORRECTNESS REGIME", + "- Reference: kestrel 0.9.1 / kestrel-kernels 0.7.4 (Photon's engine), CPU, float32, torch threads 1,", + " via scripts/dump_reference_parakeet_kestrel.py at moondream/parakeet-ultra@73175eb.", + "- C++: models/parakeet-ultra/parakeet-ultra-F32.gguf, --backend cpu --threads 1, F32 compute.", + "- Attention: TRANSCRIBE_NO_FLASH=1 (manifest cpp_env). The flash path casts the rel-pos score bias", + " to F16 (~1e-3 rel on enc.final); it is the production speed path and is gated by WER, not here.", + "- KV dtype: F32 (AUTO resolves F32 on F32 weights).", + "- Mel: production C++ MelFrontend (no injection).", + "- Cases: jfk (encoder + decode), dots (35 s) and dots-full (306 s) through the long-form path", + " (VAD head per 120 s block, pause segmentation, per-segment encoder).", + "", + "DOMINANT DRIFT SOURCES", + "- Frontend: the C++ mel is within 2.4e-6 of an fp64 recompute of kestrel's parakeet_features;", + " kestrel's own float32 torch.stft is off by up to 2.6e-4 in near-silent bins (raw log-mel ~-16,", + " where log amplifies float32 FFT error in tiny powers). All *.mel / *.mel.in drift is that", + " reference-side noise.", + "- Encoder: float32 accumulation order (kestrel fused CPU kernels vs ggml). With an identical", + " mel input, seg.6.enc.final still differs by 1.6e-5 at one element (6e-5 rel; mean 5e-8).", + "", + "WIDENED VS STAGE-2 PROVISIONAL (recipe: max(1.5 x observed, provisional, 1e-6))", + "- enc.mel.in: 0.000263 -> 0.000395 (observed 0.000264). Mechanism: reference float32 STFT noise (see frontend above).", + "- seg.0.enc.mel.in: 0.00025 -> 0.000524 (observed 0.000349). Mechanism: reference float32 STFT noise (see frontend above).", + "- seg.5.enc.final: 9.56e-06 -> 1.17e-05 (observed 7.78e-06). Mechanism: float32 accumulation order (see encoder above); localized to a few rows, mean_abs ~5e-8.", + "- seg.6.enc.final: 9.29e-06 -> 3.38e-05 (observed 2.25e-05). Mechanism: float32 accumulation order (see encoder above); localized to a few rows, mean_abs ~5e-8.", + "- vad.b0.mel: 0.00026 -> 0.000511 (observed 0.00034). Mechanism: reference float32 STFT noise (see frontend above).", + "", + "TIGHTENED (1.5 x observed): dec.joint.0 (observed 8.4e-5 / 4.0e-5); dec.lstm.* to the family 1e-6/1e-7 (observed <= 1.8e-7).", + "", + "EXACT ENTRIES", + "- dec.embed.0: blank-row embedding lookup (zeros on both sides).", + "- longform.segments: segment cut points (start sample, count); the segmenter must match kestrel", + " sample for sample.", + "", + "STRUCTURAL FIXES FOUND IN STAGE 4 (not absorbed into tolerances)", + "- Subsampler masking: kestrel zeroes the padded time tail after each strided conv, before the", + " pointwise conv, at every batch size (KV stt.parakeet.encoder.length_masking=kestrel).", + "- Pre_encode pointwise convs ran ggml_conv_2d's F16 im2col (3.3e-4 rel); ultra uses conv_2d_f32.", + "- Valid lengths from n_samples//hop; a trailing invalid encoder frame is masked and not decoded." + ], + "dec.embed.0": { + "max_abs": 0.0, + "mean_abs": 0.0 + }, + "dec.enc_out": { + "max_abs": 8.73e-06, + "mean_abs": 1e-06 + }, + "dec.joint.0": { + "max_abs": 0.00013, + "mean_abs": 6.1e-05 + }, + "dec.lstm.0.c.0": { + "max_abs": 1e-06, + "mean_abs": 1e-07 + }, + "dec.lstm.0.h.0": { + "max_abs": 1e-06, + "mean_abs": 1e-07 + }, + "dec.lstm.1.c.0": { + "max_abs": 1e-06, + "mean_abs": 1e-07 + }, + "dec.lstm.1.h.0": { + "max_abs": 1e-06, + "mean_abs": 1e-07 + }, + "enc.block.0.attn": { + "max_abs": 0.1345, + "mean_abs": 0.00383 + }, + "enc.block.0.conv": { + "max_abs": 0.1277, + "mean_abs": 0.00382 + }, + "enc.block.0.ff1": { + "max_abs": 0.1391, + "mean_abs": 0.00398 + }, + "enc.block.0.ff2": { + "max_abs": 0.1255, + "mean_abs": 0.00373 + }, + "enc.block.0.out": { + "max_abs": 0.003343, + "mean_abs": 9.838e-05 + }, + "enc.block.12.out": { + "max_abs": 0.00363, + "mean_abs": 0.0001133 + }, + "enc.block.23.out": { + "max_abs": 8.73e-06, + "mean_abs": 1e-06 + }, + "enc.final": { + "max_abs": 8.73e-06, + "mean_abs": 1e-06 + }, + "enc.mel.in": { + "max_abs": 0.0003952, + "mean_abs": 9.991e-06 + }, + "enc.pos_emb": { + "max_abs": 0.0001, + "mean_abs": 7.071e-06 + }, + "enc.pre_encode.out": { + "max_abs": 0.1332, + "mean_abs": 0.003786 + }, + "longform.segments": { + "max_abs": 0.0, + "mean_abs": 0.0 + }, + "seg.0.enc.final": { + "max_abs": 9.665e-06, + "mean_abs": 1e-06 + }, + "seg.0.enc.mel.in": { + "max_abs": 0.0005235, + "mean_abs": 9.997e-06 + }, + "seg.1.enc.final": { + "max_abs": 9.671e-06, + "mean_abs": 1e-06 + }, + "seg.1.enc.mel.in": { + "max_abs": 0.0002647, + "mean_abs": 9.996e-06 + }, + "seg.10.enc.final": { + "max_abs": 9.105e-06, + "mean_abs": 1e-06 + }, + "seg.10.enc.mel.in": { + "max_abs": 0.0002532, + "mean_abs": 9.996e-06 + }, + "seg.2.enc.final": { + "max_abs": 9.217e-06, + "mean_abs": 1e-06 + }, + "seg.2.enc.mel.in": { + "max_abs": 0.0002663, + "mean_abs": 9.996e-06 + }, + "seg.3.enc.final": { + "max_abs": 1.02e-05, + "mean_abs": 1e-06 + }, + "seg.3.enc.mel.in": { + "max_abs": 0.0002681, + "mean_abs": 9.996e-06 + }, + "seg.4.enc.final": { + "max_abs": 9.487e-06, + "mean_abs": 1e-06 + }, + "seg.4.enc.mel.in": { + "max_abs": 0.0002531, + "mean_abs": 9.996e-06 + }, + "seg.5.enc.final": { + "max_abs": 1.167e-05, + "mean_abs": 1e-06 + }, + "seg.5.enc.mel.in": { + "max_abs": 0.0002627, + "mean_abs": 9.996e-06 + }, + "seg.6.enc.final": { + "max_abs": 3.375e-05, + "mean_abs": 1e-06 + }, + "seg.6.enc.mel.in": { + "max_abs": 0.0002563, + "mean_abs": 9.997e-06 + }, + "seg.7.enc.final": { + "max_abs": 9.294e-06, + "mean_abs": 1e-06 + }, + "seg.7.enc.mel.in": { + "max_abs": 0.0002527, + "mean_abs": 9.997e-06 + }, + "seg.8.enc.final": { + "max_abs": 9.178e-06, + "mean_abs": 1e-06 + }, + "seg.8.enc.mel.in": { + "max_abs": 0.000256, + "mean_abs": 9.996e-06 + }, + "seg.9.enc.final": { + "max_abs": 9.379e-06, + "mean_abs": 1e-06 + }, + "seg.9.enc.mel.in": { + "max_abs": 0.0002346, + "mean_abs": 9.997e-06 + }, + "vad.b0.ctx": { + "max_abs": 0.01971, + "mean_abs": 0.0003352 + }, + "vad.b0.mel": { + "max_abs": 0.0005107, + "mean_abs": 9.999e-06 + }, + "vad.b0.pre_encode": { + "max_abs": 0.1404, + "mean_abs": 0.003832 + }, + "vad.b0.proj": { + "max_abs": 0.06315, + "mean_abs": 0.001473 + }, + "vad.b1.ctx": { + "max_abs": 0.01855, + "mean_abs": 0.0003185 + }, + "vad.b1.mel": { + "max_abs": 0.0002578, + "mean_abs": 9.999e-06 + }, + "vad.b1.pre_encode": { + "max_abs": 0.1379, + "mean_abs": 0.003835 + }, + "vad.b1.proj": { + "max_abs": 0.06226, + "mean_abs": 0.001481 + }, + "vad.b2.ctx": { + "max_abs": 0.01804, + "mean_abs": 0.0003066 + }, + "vad.b2.mel": { + "max_abs": 0.0002376, + "mean_abs": 9.998e-06 + }, + "vad.b2.pre_encode": { + "max_abs": 0.1386, + "mean_abs": 0.003776 + }, + "vad.b2.proj": { + "max_abs": 0.05961, + "mean_abs": 0.001443 + }, + "vad.prob": { + "max_abs": 0.0001, + "mean_abs": 9.161e-06 + } +} diff --git a/tools/transcribe-quantize/policy.cpp b/tools/transcribe-quantize/policy.cpp index f908dfa80..ea81c8f08 100644 --- a/tools/transcribe-quantize/policy.cpp +++ b/tools/transcribe-quantize/policy.cpp @@ -24,6 +24,10 @@ inline bool ends_with(const std::string & s, const char * suffix) { return s.size() >= n && std::memcmp(s.data() + s.size() - n, suffix, n) == 0; } +inline bool starts_with(const std::string & s, const char * prefix) { + return s.rfind(prefix, 0) == 0; +} + // Case-insensitive C-string equality. bool iequals(const char * a, const char * b) { while (*a && *b) { @@ -192,6 +196,13 @@ Bucket classify_tensor(const std::string & name, int64_t ne0) { contains(name, "enc.blocks.")) { return ne0 == 1 || ne0 < 0 ? Bucket::ConvPw : Bucket::Linear; } + // Parakeet VAD head: three Conv1d kernels on the subsampler + // output (vad.{proj,ctx,out}.weight). Conv keeps them F32 in every + // preset: the head is 0.2M params, and a quantized head could move + // speech-threshold crossings and with them the long-form cut points. + if (starts_with(name, "vad.") && ends_with(name, ".weight")) { + return Bucket::Conv; + } // Conv kernels: enc.pre_encode.conv.{0,2,3,5,6}.weight and // enc.blocks.{i}.conv.depthwise.weight. // ".conv." (with dots on both sides) distinguishes these from