py-mac-speech-analyzer 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ name: "Publish release to PyPI"
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ # Publish on any tag starting with a `v`, e.g., v0.1.0
7
+ - v*
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: macos-26
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - uses: maxim-lobanov/setup-xcode@v1
16
+ with:
17
+ xcode-version: latest-stable
18
+ - run: uv build
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ permissions:
28
+ id-token: write
29
+ steps:
30
+ - uses: actions/download-artifact@v4
31
+ with: { name: dist, path: dist/ }
32
+ - uses: astral-sh/setup-uv@v7
33
+ - run: uv publish
@@ -0,0 +1,21 @@
1
+ name: "Forwarding shim to PyPI"
2
+
3
+ on:
4
+ push:
5
+ tags: ["shim-v*"]
6
+
7
+ defaults:
8
+ run:
9
+ working-directory: shim
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write # PyPI trusted publishing
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v7
20
+ - run: uv build
21
+ - run: uv publish
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,19 @@
1
+ cmake_minimum_required(VERSION 3.26)
2
+ project(macspeechanalyzer LANGUAGES Swift)
3
+
4
+ add_library(macspeechanalyzer_core SHARED
5
+ swift/core.swift
6
+ swift/bridge.swift)
7
+
8
+ # Editable/inplace: no install step runs, so aim the linker output
9
+ # directly at the package. $<0:> keeps multi-config generators happy.
10
+ if(SKBUILD_STATE STREQUAL "editable")
11
+ set_target_properties(macspeechanalyzer_core PROPERTIES
12
+ LIBRARY_OUTPUT_DIRECTORY
13
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/macspeechanalyzer/$<0:>")
14
+ endif()
15
+
16
+ # Wheel builds: install step runs, destination is relative to the wheel root
17
+ install(TARGETS macspeechanalyzer_core
18
+ LIBRARY DESTINATION macspeechanalyzer
19
+ RUNTIME DESTINATION macspeechanalyzer)
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 paraversal
4
+ Copyright (c) 2025 Argmax, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-mac-speech-analyzer
3
+ Version: 0.2.0
4
+ Summary: On-device speech transcription for macOS via Apple's SpeechAnalyzer
5
+ License-Expression: MIT
6
+ Classifier: Operating System :: MacOS
7
+ Classifier: Programming Language :: Python :: 3
8
+ Project-URL: Homepage, https://github.com/paraversal/py-mac-speech-analyzer
9
+ Project-URL: Issues, https://github.com/paraversal/py-mac-speech-analyzer/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # py-mac-speech-analyzer
14
+
15
+ 🐍 Python package to run the macOS 26 SpeechAnalyzer API from Python to transcribe audio files.
16
+
17
+ The SpeechAnalyzer API generally provides [faster and better results]((https://get-inscribe.com/blog/apple-speech-api-benchmark.html)) than other locally running transcription engines of comparable size.
18
+
19
+ ## Features
20
+
21
+ - **On-device transcription**. Audio never leaves your Mac.
22
+ - **Supports many languages**. Automatic model download on first use.
23
+ - **Progressive output**. Observe segments as they're finalized, or wait for the full transcript.
24
+
25
+ ## Requirements
26
+
27
+ - macOS >= 26.0
28
+ - XCode command-line tools
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ uv add py-mac-speech-analyzer
34
+ # or
35
+ pip install py-mac-speech-analyzer
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Basic usage
41
+
42
+ ```py
43
+ from macspeechanalyzer import transcribe
44
+
45
+ text = transcribe("/path/to/file", locale="en")
46
+ ```
47
+
48
+ ### Progressive transcription
49
+
50
+ `transcribe` returns the full transcript once analysis completes, but you can also observe segments as they're finalized. Pass any callable as on_segment and it'll be invoked once per segment, in order, with the segment text:
51
+
52
+ ```py
53
+ from macspeechanalyzer import transcribe
54
+
55
+ # default: no progressive output
56
+ text = transcribe("/path/to/file", locale="en")
57
+
58
+ # echo each segment to stdout as it's transcribed
59
+ text = transcribe("/path/to/file", locale="en", on_segment=print)
60
+
61
+ # or use your own callback
62
+ def log(segment: str) -> None:
63
+ print(f"... {segment} ...")
64
+
65
+ text = transcribe("/path/to/file", locale="en", on_segment=log)
66
+ ```
67
+
68
+ Segments are non-overlapping and never revised, so appending them yields the same string as the return value.
69
+
70
+ ## Technical details
71
+
72
+ This package is made up of two parts: the Python wrapper provides the API, but the actual functionality is achieved by embedding a Swift project which exposes functions to C. With this kind of three language interplay, a clean separation of concerns is really important. Therefore, the implementation is divided into the following four layers:
73
+
74
+ - Pure Python
75
+ - Python → C compat
76
+ - Swift → C compat
77
+ - Pure Swift
78
+
79
+ The pure Swift and pure Python layers only deal with their respective languages & types, while the compat layers do all the ugly work of converting between native and C-compatible types
80
+
81
+ ## Roadmap
82
+
83
+ - ✅ Basic one-shot transcription
84
+ - Class-based interface
85
+ - Context manager implementation
86
+ - Transcription of live audio stream (microphone, ...)
87
+
88
+
89
+ ## Development
90
+
91
+ ```zsh
92
+ uv pip install -e .
93
+ ```
94
+
95
+ to one-time-install the package as editable, from then on:
96
+
97
+ ```zsh
98
+ ninja
99
+ ```
100
+
101
+ to rebuild the package
102
+
103
+ # License
104
+
105
+ py-mac-speech-analyzer is released under the MIT License. See [LICENSE](./LICENSE) for more details.
@@ -0,0 +1,19 @@
1
+ import PackageDescription
2
+
3
+ let package = Package(
4
+ name: "mac-speech-analyzer",
5
+ platforms: [ .macOS(.v26) ],
6
+ targets: [
7
+ .executableTarget(
8
+ name: "mac-speech-analyzer",
9
+ linkerSettings: [
10
+ .unsafeFlags([
11
+ "-Xlinker","-sectcreate",
12
+ "-Xlinker","__TEXT",
13
+ "-Xlinker","__info_plist",
14
+ "-Xlinker","Info.plist"
15
+ ])
16
+ ]
17
+ )
18
+ ]
19
+ )
@@ -0,0 +1,93 @@
1
+ # py-mac-speech-analyzer
2
+
3
+ 🐍 Python package to run the macOS 26 SpeechAnalyzer API from Python to transcribe audio files.
4
+
5
+ The SpeechAnalyzer API generally provides [faster and better results]((https://get-inscribe.com/blog/apple-speech-api-benchmark.html)) than other locally running transcription engines of comparable size.
6
+
7
+ ## Features
8
+
9
+ - **On-device transcription**. Audio never leaves your Mac.
10
+ - **Supports many languages**. Automatic model download on first use.
11
+ - **Progressive output**. Observe segments as they're finalized, or wait for the full transcript.
12
+
13
+ ## Requirements
14
+
15
+ - macOS >= 26.0
16
+ - XCode command-line tools
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ uv add py-mac-speech-analyzer
22
+ # or
23
+ pip install py-mac-speech-analyzer
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Basic usage
29
+
30
+ ```py
31
+ from macspeechanalyzer import transcribe
32
+
33
+ text = transcribe("/path/to/file", locale="en")
34
+ ```
35
+
36
+ ### Progressive transcription
37
+
38
+ `transcribe` returns the full transcript once analysis completes, but you can also observe segments as they're finalized. Pass any callable as on_segment and it'll be invoked once per segment, in order, with the segment text:
39
+
40
+ ```py
41
+ from macspeechanalyzer import transcribe
42
+
43
+ # default: no progressive output
44
+ text = transcribe("/path/to/file", locale="en")
45
+
46
+ # echo each segment to stdout as it's transcribed
47
+ text = transcribe("/path/to/file", locale="en", on_segment=print)
48
+
49
+ # or use your own callback
50
+ def log(segment: str) -> None:
51
+ print(f"... {segment} ...")
52
+
53
+ text = transcribe("/path/to/file", locale="en", on_segment=log)
54
+ ```
55
+
56
+ Segments are non-overlapping and never revised, so appending them yields the same string as the return value.
57
+
58
+ ## Technical details
59
+
60
+ This package is made up of two parts: the Python wrapper provides the API, but the actual functionality is achieved by embedding a Swift project which exposes functions to C. With this kind of three language interplay, a clean separation of concerns is really important. Therefore, the implementation is divided into the following four layers:
61
+
62
+ - Pure Python
63
+ - Python → C compat
64
+ - Swift → C compat
65
+ - Pure Swift
66
+
67
+ The pure Swift and pure Python layers only deal with their respective languages & types, while the compat layers do all the ugly work of converting between native and C-compatible types
68
+
69
+ ## Roadmap
70
+
71
+ - ✅ Basic one-shot transcription
72
+ - Class-based interface
73
+ - Context manager implementation
74
+ - Transcription of live audio stream (microphone, ...)
75
+
76
+
77
+ ## Development
78
+
79
+ ```zsh
80
+ uv pip install -e .
81
+ ```
82
+
83
+ to one-time-install the package as editable, from then on:
84
+
85
+ ```zsh
86
+ ninja
87
+ ```
88
+
89
+ to rebuild the package
90
+
91
+ # License
92
+
93
+ py-mac-speech-analyzer is released under the MIT License. See [LICENSE](./LICENSE) for more details.
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
+ <plist version="1.0">
5
+ <dict>
6
+ <key>com.apple.developer.speech-recognition</key>
7
+ <true/>
8
+ <key>com.apple.security.cs.disable-library-validation</key>
9
+ <true/>
10
+ </dict>
11
+ </plist>
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.10"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "py-mac-speech-analyzer"
7
+ version = "0.2.0"
8
+ description = "On-device speech transcription for macOS via Apple's SpeechAnalyzer"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Operating System :: MacOS",
14
+ "Programming Language :: Python :: 3"
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/paraversal/py-mac-speech-analyzer"
19
+ Issues = "https://github.com/paraversal/py-mac-speech-analyzer/issues"
20
+
21
+ [tool.scikit-build]
22
+ wheel.packages = ["src/macspeechanalyzer"]
23
+ wheel.py-api = "py3"
24
+ minimum-version = "build-system.requires"
25
+ build-dir = "build/{wheel_tag}"
26
+
27
+ [tool.scikit-build.cmake]
28
+ version = ">=3.26"
29
+
30
+ [tool.scikit-build.editable]
31
+ mode = "inplace"
32
+ rebuild = true
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "cmake>=4.4.0",
37
+ "ninja>=1.13.0",
38
+ "scikit-build-core>=1.0.3",
39
+ ]
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "mac-speech-analyzer"
3
+ version = "0.1.3"
4
+ description = "Renamed to py-mac-speech-analyzer — this is a forwarding package."
5
+ requires-python = ">=3.10"
6
+ dependencies = ["py-mac-speech-analyzer>=0.2.0"]
7
+
8
+ [project.urls]
9
+ Homepage = "https://github.com/paraversal/py-mac-speech-analyzer"
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ bypass-selection = true
@@ -0,0 +1,4 @@
1
+ import logging
2
+ from macspeechanalyzer.main import transcribe
3
+
4
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,116 @@
1
+ import ctypes
2
+ from pathlib import Path
3
+ import logging
4
+ from typing import Callable
5
+ import os
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ LOG_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
10
+ SEGMENT_CB = ctypes.CFUNCTYPE(None, ctypes.c_char_p)
11
+
12
+ @LOG_CB
13
+ def _log_cb(level, msg):
14
+ # for now, hacky: newline and flush stdout before/after each log print, otherwise logs may be bolted straight to a segment
15
+ print("", flush=True)
16
+ log.log(level or logging.INFO, msg.decode("utf-8", "replace") if msg else "")
17
+
18
+ @SEGMENT_CB
19
+ def _live_cb(msg):
20
+ print(msg.decode("utf-8", "replace") if msg else "", end="", flush=True)
21
+
22
+ _lib = ctypes.CDLL(str(Path(__file__).parent / f"libmacspeechanalyzer_core.dylib"))
23
+
24
+ # int32 msa_transcribe(const char* url, const char* locale, char** outText, bool* emitLiveFragments)
25
+ _lib.msa_transcribe.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_bool]
26
+ _lib.msa_transcribe.restype = ctypes.c_int32
27
+
28
+ # void msa_free_string(char* ptr)
29
+ _lib.msa_free_string.argtypes = [ctypes.c_void_p]
30
+ _lib.msa_free_string.restype = None
31
+
32
+ # char* msa_last_error() — strdup'ed message, caller frees
33
+ _lib.msa_last_error.argtypes = []
34
+ _lib.msa_last_error.restype = ctypes.c_void_p
35
+
36
+ _lib.msa_set_log_callback.argtypes = [LOG_CB]
37
+ _lib.msa_set_log_callback.restype = None
38
+ _lib.msa_set_log_callback(_log_cb)
39
+
40
+ _lib.msa_set_live_callback.argtypes = [SEGMENT_CB]
41
+ _lib.msa_set_live_callback.restype = None
42
+
43
+ class SpeechAnalyzerError(Exception):
44
+ pass
45
+
46
+ def _take_string(ptr: ctypes.c_void_p) -> str:
47
+ """Copy a Swift-allocated C string into a Python str, then free it."""
48
+ try:
49
+ return ctypes.string_at(ptr).decode("utf-8")
50
+ finally:
51
+ _lib.msa_free_string(ptr)
52
+
53
+ def _raise_last_error(status: int) -> None:
54
+ ptr = _lib.msa_last_error()
55
+ msg = _take_string(ctypes.c_void_p(ptr)) if ptr else f"native error (status {status})"
56
+ raise SpeechAnalyzerError(msg)
57
+
58
+ def transcribe(path: str | Path, locale: str | None = None, on_segment: Callable[[str], None] | None = None) -> str:
59
+ """Transcribe an audio file on-device.
60
+
61
+ Runs synchronously and returns the full transcript once the file has
62
+ been fully analyzed. If ``on_segment`` is provided, it is also invoked
63
+ once per finalized segment as transcription progresses, allowing the
64
+ caller to observe or display partial results without waiting for the
65
+ final return value.
66
+
67
+ Args:
68
+ path: Filesystem path to the audio file to transcribe. Any format
69
+ readable by ``AVAudioFile`` is accepted (e.g. wav, m4a, mp3,
70
+ caf, aiff).
71
+ locale: BCP 47 / Apple locale identifier (e.g. ``"en_US"``,
72
+ ``"de_DE"``, ``"ja_JP"``) selecting the transcription language.
73
+ If ``None``, the system's current locale is used when supported,
74
+ otherwise English. If the requested locale's model is not yet
75
+ installed on the device, it will be downloaded on first use.
76
+ on_segment: Optional callback invoked from a background thread once
77
+ per finalized transcript segment, in order, with the segment
78
+ text as its single argument. Segments are non-overlapping and
79
+ never revised, so appending them reconstructs the full
80
+ transcript. Pass ``print`` to echo segments to stdout as they
81
+ arrive.
82
+
83
+ Returns:
84
+ The complete transcript as a single string.
85
+
86
+ Raises:
87
+ FileNotFoundError: If ``url`` does not point to a readable file.
88
+ RuntimeError: If the host is running a macOS version older than
89
+ 26.0, or if the native transcription pipeline fails.
90
+ """
91
+ emit_live_segments = False
92
+ if on_segment is print:
93
+ _lib.msa_set_live_callback(_live_cb)
94
+ emit_live_segments = True
95
+ elif on_segment is not None:
96
+ def _trampoline(raw: bytes | None) -> None:
97
+ if raw is not None:
98
+ on_segment(raw.decode("utf-8", "replace"))
99
+ cb = SEGMENT_CB(_trampoline)
100
+ _lib.msa_set_live_callback(cb)
101
+ emit_live_segments = True
102
+
103
+ path = os.fspath(path)
104
+
105
+ out = ctypes.c_void_p()
106
+ status = _lib.msa_transcribe(
107
+ path.encode("utf-8"),
108
+ locale.encode("utf-8") if locale is not None else None,
109
+ ctypes.byref(out), # &out -> char**
110
+ emit_live_segments
111
+ )
112
+ if status != 0:
113
+ _raise_last_error(status)
114
+ if not out.value:
115
+ raise SpeechAnalyzerError("transcribe returned success but no text")
116
+ return _take_string(out)
@@ -0,0 +1,87 @@
1
+ import Foundation
2
+
3
+ // Async compat
4
+ final class ResultBox<T>: @unchecked Sendable {
5
+ var value: Result<T, Error>?
6
+ }
7
+
8
+ func runBlocking<T: Sendable>(_ body: @Sendable @escaping () async throws -> T) throws -> T {
9
+ let box = ResultBox<T>()
10
+ let semaphore = DispatchSemaphore(value: 0)
11
+ Task.detached {
12
+ do { box.value = .success(try await body()) }
13
+ catch { box.value = .failure(error) }
14
+ semaphore.signal()
15
+ }
16
+ semaphore.wait()
17
+ return try box.value!.get()
18
+ }
19
+
20
+ private let errorLock = NSLock()
21
+ private var _lastError: String?
22
+
23
+ func setLastError(_ message: String) {
24
+ errorLock.lock(); defer { errorLock.unlock() }
25
+ _lastError = message
26
+ }
27
+
28
+ @_cdecl("msa_last_error")
29
+ public func msaLastError() -> UnsafeMutablePointer<CChar>? {
30
+ errorLock.lock(); defer { errorLock.unlock() }
31
+ guard let err = _lastError else { return nil }
32
+ return strdup(err) // Python copies + msa_free_string's it
33
+ }
34
+
35
+ @_cdecl("msa_transcribe")
36
+ public func msaTranscribe(
37
+ _ url: UnsafePointer<CChar>?,
38
+ _ locale: UnsafePointer<CChar>?,
39
+ _ outText: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
40
+ _ emitLiveSegments: CBool) -> Int32 {
41
+ let localeStr = locale.map { String(cString: $0) } // nil stays nil
42
+ let urlStr = url.map { String(cString: $0) }!
43
+ do {
44
+ let text = try runBlocking { try await transcribe(url: urlStr, locale: localeStr, emitLiveSegments: emitLiveSegments) }
45
+ outText?.pointee = strdup(text)
46
+ return 0
47
+ } catch {
48
+ setLastError("\(error)")
49
+ return 1
50
+ }
51
+ }
52
+
53
+ @_cdecl("msa_free_string")
54
+ public func msaFreeString(_ ptr: UnsafeMutablePointer<CChar>?) {
55
+ free(ptr)
56
+ }
57
+
58
+ public typealias MSALogCallback = @convention(c) (Int32, UnsafePointer<CChar>?) -> Void
59
+ private var logSink: MSALogCallback?
60
+
61
+ @_cdecl("msa_set_log_callback")
62
+ public func msa_set_log_callback(_ cb: MSALogCallback?) {
63
+ logSink = cb
64
+ }
65
+
66
+ // use this everywhere instead of print()
67
+ func msaLog(_ level: Int32, _ message: String) {
68
+ if let sink = logSink {
69
+ message.withCString { sink(level, $0) }
70
+ } else {
71
+ print(message) // fallback when nobody registered
72
+ }
73
+ }
74
+
75
+ public typealias MSALiveTranscription = @convention(c) (UnsafePointer<CChar>?) -> Void
76
+ private var liveTranscriptionSink: MSALiveTranscription?
77
+
78
+ @_cdecl("msa_set_live_callback")
79
+ public func msa_set_live_transcription_callback(_ cb: MSALiveTranscription?) {
80
+ liveTranscriptionSink = cb
81
+ }
82
+
83
+ func msaLiveTranscriptionSendFragment(_ message: String) {
84
+ if let sink = liveTranscriptionSink {
85
+ message.withCString { sink($0) }
86
+ }
87
+ }
@@ -0,0 +1,95 @@
1
+ import Foundation
2
+ import AVFAudio
3
+ import Speech
4
+ import Darwin
5
+
6
+ let INFO = Int32(20)
7
+
8
+ enum MSAError: Error {
9
+ case osRequirementError(String)
10
+ }
11
+
12
+ func checkMacosVersion () throws {
13
+ guard #available(macOS 26.0, *) else {
14
+ throw MSAError.osRequirementError("SpeechAnalyzer is only available on macos >= 26.0.")
15
+ }
16
+ }
17
+
18
+ func transcribe(url: String, locale: String?, emitLiveSegments: Bool) async throws -> String {
19
+ try checkMacosVersion()
20
+ let locale = await getLocale(customLocale: locale)
21
+
22
+ let transcriber = SpeechTranscriber(
23
+ locale: locale,
24
+ preset: .transcription
25
+ )
26
+ do {
27
+ try await downloadLocale(locale, transcriber)
28
+ } catch {
29
+ msaLog(INFO, "so this failed...")
30
+ }
31
+
32
+ let analyzer = SpeechAnalyzer(modules: [transcriber])
33
+ let inputURL = URL(fileURLWithPath: url)
34
+ let audioFile = try AVAudioFile(forReading: inputURL)
35
+
36
+ var transcript = ""
37
+ let consumer = Task {
38
+ for try await result in transcriber.results where result.isFinal {
39
+ let piece = String(result.text.characters)
40
+ if emitLiveSegments {
41
+ msaLiveTranscriptionSendFragment(piece)
42
+ }
43
+ transcript += piece
44
+ }
45
+ return transcript
46
+ }
47
+
48
+ if let last = try await analyzer.analyzeSequence(from: audioFile) {
49
+ try await analyzer.finalizeAndFinish(through: last)
50
+ } else {
51
+ await analyzer.cancelAndFinishNow()
52
+ }
53
+ try await consumer.value
54
+
55
+ msaLog(INFO, "Successfully transcribed text.")
56
+ return transcript
57
+ }
58
+
59
+ /// Obtains a valid locale (e.g., en_US).
60
+ /// Only a subset of all available locales are supported by the SpeechAnalyzer API.
61
+ /// 1. If a custom locale was passed, there is an attempt to parse it.
62
+ /// 2. If there was no custom locale passed or the parsing failed, we try to use the system locale.
63
+ /// 3. If this also fails, we use the `en` locale
64
+ func getLocale(customLocale: String?) async -> Locale {
65
+ var locale: Locale
66
+ // if a custom locale was passed; try to parse it
67
+ if let setCustomLocaleString = customLocale {
68
+ let setCustomLocale = Locale.init(identifier: setCustomLocaleString)
69
+ if let supportedCustomLocale = await SpeechTranscriber.supportedLocale(equivalentTo: setCustomLocale) {
70
+ return supportedCustomLocale
71
+ } else {
72
+ // we should probably throw an exception here
73
+ }
74
+ }
75
+
76
+ // if not, try to use current system locale
77
+ if let currentLocale = await SpeechTranscriber.supportedLocale(equivalentTo: Locale.current) {
78
+ locale = currentLocale
79
+ } else {
80
+ // if even this doesn't work, we simply use English
81
+ locale = Locale.init(languageCode: .english, script: .latin, languageRegion: .unknown)
82
+ }
83
+
84
+ return locale
85
+ }
86
+
87
+ /// Checks the availability of the selected locale, and downloads it if necessary.
88
+ func downloadLocale(_ locale: Locale, _ transcriber: SpeechTranscriber) async throws {
89
+ if !(await SpeechTranscriber.installedLocales).contains(locale) {
90
+ msaLog(INFO, "Downloading locale \(locale)")
91
+ if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
92
+ try await request.downloadAndInstall()
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,185 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "cmake"
7
+ version = "4.4.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ca/4c/fec299c7e437299ac354b7907bdcedfb85860afce76471ed0fcc6879166d/cmake-4.4.0.tar.gz", hash = "sha256:933eb5977ecc5226e47d702d1c412b69f77ca00f59a4660c80b436cf7d5472de", size = 34915, upload-time = "2026-07-10T20:50:32.879Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/50/21/9e97957b80e548f44277753f369bfcb9a997333380d5adb9f4dcf8cc6790/cmake-4.4.0-py3-none-macosx_10_10_universal2.whl", hash = "sha256:3068d219f21dde2c757fc65bf6fa1f11e892ddba1a00f2bc87276529d9c95f9a", size = 54381909, upload-time = "2026-07-10T20:48:51.167Z" },
12
+ { url = "https://files.pythonhosted.org/packages/a3/fc/83b3472c3c40759e29970f583a6602fb584ae0123b573de2ff44a0a52712/cmake-4.4.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7ce803007cbebd3440eb0c52cec6b28a172b5cd7737ceaf613bc566880c1d0a4", size = 30277284, upload-time = "2026-07-10T20:48:57.604Z" },
13
+ { url = "https://files.pythonhosted.org/packages/3d/36/0c83d4682758f6e1cbc56f0aaac5b392a3c93d81518d99418aa0f2e1d805/cmake-4.4.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:41021341a394406fa56d54dabe2eaa32ac7f74fde076720397872bdc3f96a19d", size = 31451960, upload-time = "2026-07-10T20:49:02.31Z" },
14
+ { url = "https://files.pythonhosted.org/packages/22/fc/33224d3f8310555e26934a119a681b76e6324601e5617b88515b2aecddd5/cmake-4.4.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:825ce04c2873b4d1e6185d7cf8330d38658b00d72e56a375445241d14fe21ff7", size = 31208506, upload-time = "2026-07-10T20:49:07.954Z" },
15
+ { url = "https://files.pythonhosted.org/packages/fe/86/640651cd15993ce76f54bf953cc39c1f0114f5d22960301b208887c0b8d5/cmake-4.4.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4329555c90091d10c4651272c36c8fe91b7af8c25655529c8276c33461d02957", size = 29087073, upload-time = "2026-07-10T20:49:12.731Z" },
16
+ { url = "https://files.pythonhosted.org/packages/f8/8d/6f62747b52bf1b48f05f1bf187ea95bfbc6338d21692223f67db2576b944/cmake-4.4.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51bcb2e65c0d5c1af4a749ba40a321b7436420c7359724ab46719004f3cd2149", size = 30215347, upload-time = "2026-07-10T20:49:18.265Z" },
17
+ { url = "https://files.pythonhosted.org/packages/f1/5a/370b663877b0f58083fdcf1c5490604d0d4bf7750d63169e1c49c7035a5e/cmake-4.4.0-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:a70cafba0e4f7a741b8ef41fc8a58da0963512a0f505a38256ed0a2c70ef07eb", size = 27309126, upload-time = "2026-07-10T20:49:23.161Z" },
18
+ { url = "https://files.pythonhosted.org/packages/2f/63/83bf7eacccf338d939098f5f6944eed3b27b7da2c56442eb7640c2468f7d/cmake-4.4.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a550aaae137eaed453eb4493ceafa135f70da0b6660cd3ef05adbf9a2e9da629", size = 27419954, upload-time = "2026-07-10T20:49:28.13Z" },
19
+ { url = "https://files.pythonhosted.org/packages/5e/f6/4a9158c97cbbc73a99102228914453f71a429cc05e54193bdb94306d6679/cmake-4.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43d07406792836dc27d3d17f0ef927aa1f07f9cabf19674bf3daf64998a1a4c1", size = 39458539, upload-time = "2026-07-10T20:49:34.228Z" },
20
+ { url = "https://files.pythonhosted.org/packages/29/8c/86cf5eb866dabc1e19c64ea0e2445504b32633a609d54ae78537a9374d11/cmake-4.4.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:efaca203845fdb611e2709fc88e39034d9596139e7ef8c87875ff8b63728576e", size = 36091712, upload-time = "2026-07-10T20:49:40.29Z" },
21
+ { url = "https://files.pythonhosted.org/packages/1e/37/b23658343f7c00ced44813dd1dd272840c733601fb8c05d99e8e7b1f6246/cmake-4.4.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2df0e65b29fd68067ace39a5405ce334a6f3a738ae59f9ca50f79bee178d1ae0", size = 42227957, upload-time = "2026-07-10T20:49:46.901Z" },
22
+ { url = "https://files.pythonhosted.org/packages/f2/b4/f197a34feaee62b5ffd0091cce8bb89b46420ae322fd0ba503829b08f1a0/cmake-4.4.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3e9260372bbdc68f4b575548ca92ba02c459ed6acfc1903fae49d8945afe529b", size = 41381016, upload-time = "2026-07-10T20:49:53.213Z" },
23
+ { url = "https://files.pythonhosted.org/packages/97/d9/6deaf3453ce1c60d72cdfea8b3fe05cf367578f5610ed96d5548e3019970/cmake-4.4.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:fc30f2cffc29951ba996db0f73a5f6cb4e4ef96be983199a10c2eeb25c4f151e", size = 36306195, upload-time = "2026-07-10T20:49:59.07Z" },
24
+ { url = "https://files.pythonhosted.org/packages/8b/0f/2c397d6712fac6ed892d2614ed08947b78714aeca53ccb6c58b62730304c/cmake-4.4.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:66271dacfa14a1a6abd845b65ae76dc14491716c3fdaa499745e6d16f41712c8", size = 38652910, upload-time = "2026-07-10T20:50:05.141Z" },
25
+ { url = "https://files.pythonhosted.org/packages/f1/85/24cb4bf15eecfb7bf8092bccbe6263c4ff1cd22ce88077fb5131f5279d16/cmake-4.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842de4296c7fb651799e6e3c1f8dc2355d3539067507f5c8012d1dc4625657b2", size = 39428686, upload-time = "2026-07-10T20:50:11.299Z" },
26
+ { url = "https://files.pythonhosted.org/packages/ca/51/f2057dcfdd2d87e98a7ddd4347111165612780a9da872195f538d947e869/cmake-4.4.0-py3-none-win32.whl", hash = "sha256:8e245108da252fc67410b549f025d578d7689f99366e00d8cea1f6a1c49fc1a9", size = 38759093, upload-time = "2026-07-10T20:50:17.491Z" },
27
+ { url = "https://files.pythonhosted.org/packages/6d/cb/7118b720745d76a6dca9ddf3c096fb90c71a2841b8ca493218202fc9d40a/cmake-4.4.0-py3-none-win_amd64.whl", hash = "sha256:13209a4d89d554c0b5623ed1158024b045e468bbb912c3f491de5e957b940858", size = 42309353, upload-time = "2026-07-10T20:50:23.85Z" },
28
+ { url = "https://files.pythonhosted.org/packages/04/8f/2818561f47151e0ffdfe44356c160038b5ae4e9025c0a3e61e36cd685489/cmake-4.4.0-py3-none-win_arm64.whl", hash = "sha256:7d481da3868e1b5831db34573c061daa8534a584baaffac93b1348b92e493f20", size = 40473618, upload-time = "2026-07-10T20:50:30.352Z" },
29
+ ]
30
+
31
+ [[package]]
32
+ name = "exceptiongroup"
33
+ version = "1.3.1"
34
+ source = { registry = "https://pypi.org/simple" }
35
+ dependencies = [
36
+ { name = "typing-extensions" },
37
+ ]
38
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "mac-speech-analyzer"
45
+ version = "0.1.0"
46
+ source = { editable = "." }
47
+
48
+ [package.dev-dependencies]
49
+ dev = [
50
+ { name = "cmake" },
51
+ { name = "ninja" },
52
+ { name = "scikit-build-core" },
53
+ ]
54
+
55
+ [package.metadata]
56
+
57
+ [package.metadata.requires-dev]
58
+ dev = [
59
+ { name = "cmake", specifier = ">=4.4.0" },
60
+ { name = "ninja", specifier = ">=1.13.0" },
61
+ { name = "scikit-build-core", specifier = ">=1.0.3" },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "ninja"
66
+ version = "1.13.0"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" }
69
+ wheels = [
70
+ { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" },
71
+ { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" },
72
+ { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" },
73
+ { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" },
74
+ { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" },
75
+ { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" },
76
+ { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" },
77
+ { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" },
78
+ { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" },
79
+ { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" },
80
+ { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" },
81
+ { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" },
82
+ { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" },
83
+ { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" },
84
+ { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" },
85
+ { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" },
86
+ { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" },
87
+ { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" },
88
+ ]
89
+
90
+ [[package]]
91
+ name = "packaging"
92
+ version = "26.2"
93
+ source = { registry = "https://pypi.org/simple" }
94
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
97
+ ]
98
+
99
+ [[package]]
100
+ name = "pathspec"
101
+ version = "1.1.1"
102
+ source = { registry = "https://pypi.org/simple" }
103
+ sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "scikit-build-core"
110
+ version = "1.0.3"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ dependencies = [
113
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
114
+ { name = "packaging" },
115
+ { name = "pathspec" },
116
+ { name = "tomli", marker = "python_full_version < '3.11'" },
117
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
118
+ ]
119
+ sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/0f69b0c7150ce4bcee78c199fe3b7e03a6e01578451bd5d5d1a58beb32f9/scikit_build_core-1.0.3.tar.gz", hash = "sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8", size = 477539, upload-time = "2026-07-12T05:08:30.298Z" }
120
+ wheels = [
121
+ { url = "https://files.pythonhosted.org/packages/b7/b0/7ad8fa9aebb0835b2e20afebf1483cbe175d1612fe01e85042ce9ced8755/scikit_build_core-1.0.3-py3-none-any.whl", hash = "sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085", size = 278294, upload-time = "2026-07-12T05:08:28.725Z" },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "tomli"
126
+ version = "2.4.1"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
131
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
132
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
133
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
134
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
135
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
136
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
137
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
138
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
139
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
140
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
141
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
142
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
143
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
144
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
145
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
146
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
147
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
148
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
149
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
150
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
151
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
152
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
153
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
154
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
155
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
156
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
157
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
158
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
159
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
160
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
161
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
162
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
163
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
164
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
165
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
166
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
167
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
168
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
169
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
170
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
171
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
172
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
173
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
174
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
175
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
176
+ ]
177
+
178
+ [[package]]
179
+ name = "typing-extensions"
180
+ version = "4.16.0"
181
+ source = { registry = "https://pypi.org/simple" }
182
+ 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" }
183
+ wheels = [
184
+ { 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" },
185
+ ]