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:)`.