1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# sous_chef
A command-line tool that transcribes a podcast audio file using Apple's
SpeechAnalyzer framework (macOS 26+, Speech framework).
## Usage
```
sous_chef <input.mp3> <output.json>
```
Reads an audio file, transcribes it using SpeechAnalyzer with the
SpeechTranscriber module, and writes the results as JSON.
## SpeechAnalyzer API overview
The Speech framework on macOS 26 introduces `SpeechAnalyzer`, an actor
that coordinates analysis modules against audio input. The module we
care about is `SpeechTranscriber`, which does speech-to-text.
### Key types
- `SpeechAnalyzer` — the actor that drives analysis. Accepts modules
and audio input, controls the session lifecycle.
- `SpeechTranscriber` — a module that produces transcription results.
Created with a locale and a preset.
- `SpeechTranscriber.Preset` — predefined configurations. We use
`.transcription` for basic accurate transcription with no timestamps.
- `SpeechTranscriber.Result` — a phrase of transcribed speech. Has:
- `.text` — an `AttributedString` with the best interpretation. Can
carry `audioTimeRange` attributes when the preset includes them.
- `.alternatives` — alternative interpretations in descending
likelihood order.
- `AssetInventory` — manages ML model downloads. Assets must be
installed before transcription can begin.
- `AnalyzerInput` — wraps an `AVAudioPCMBuffer` for the input sequence.
### File-based transcription flow
The simplest path for transcribing a file:
```swift
import Speech
import AVFoundation
// 1. Create the transcriber module.
guard let locale = SpeechTranscriber.supportedLocale(equivalentTo: Locale(identifier: "en-US")) else {
fatalError("en-US not supported")
}
let transcriber = SpeechTranscriber(locale: locale, preset: .timeIndexedTranscriptionWithAlternatives)
// 2. Ensure assets are installed.
if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
try await request.downloadAndInstall()
}
// 3. Open the audio file.
let audioFile = try AVAudioFile(forReading: URL(fileURLWithPath: inputPath))
// 4. Create the analyzer and feed it the file.
let analyzer = SpeechAnalyzer(modules: [transcriber])
let lastSampleTime = try await analyzer.analyzeSequence(from: audioFile)
// 5. Collect results concurrently.
// transcriber.results is an AsyncSequence of SpeechTranscriber.Result.
// Each result has .text (AttributedString) which you can convert to
// plain text with String(result.text.characters).
// When using timeIndexedTranscriptionWithAlternatives, the text's
// attributed string includes audioTimeRange attributes.
// 6. Finalize.
if let lastSampleTime {
try await analyzer.finalizeAndFinish(through: lastSampleTime)
} else {
try analyzer.cancelAndFinishNow()
}
```
Note that result collection (step 5) must happen concurrently with
analysis (step 4) — they run in separate tasks. The analyzer produces
results as it processes audio; the results stream ends after
finalization.
### Result structure
`SpeechTranscriber.Result.text` is an `AttributedString`. With the
`timeIndexedTranscriptionWithAlternatives` preset, each segment of text
carries a `SpeechAttributes.TimeRangeAttribute` indicating the audio
time range it corresponds to.
To get plain text: `String(result.text.characters)`
## Output format
Write plain text to the output path. Each `SpeechTranscriber.Result`
becomes one line, joined with newlines.
## Build
This is a Swift package. Build with:
```
cd sous_chef
swift build -c release
```
The binary lands at `.build/release/sous_chef`.
## Implementation notes
- Use Swift 6 and strict concurrency.
- Use `ArgumentParser` for CLI argument handling.
- The tool should print progress to stderr (e.g., "Downloading
assets...", "Transcribing...", "Done.") so stdout stays clean.
- Exit with a nonzero code on failure.
- Use `analyzeSequence(from:)` for the file-based path — it handles
audio format conversion automatically.
- Collect results by iterating `transcriber.results` in a separate
task started before calling `analyzeSequence(from:)`.