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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Whistle: speech-to-text, word timestamps and speech embedding on the Needle engine.
//!
//! ## Overview
//!
//! Whistle is not a second engine. It is a 16.9 MB speech model loaded into the same runtime as
//! Needle, which holds one model per kind, so a [`Whistle`] and a
//! [`Needle`](crate::needle::Needle) live side by side in one process. Five types carry the
//! whole API:
//!
//! | Type | What it is |
//! | --- | --- |
//! | [`Weights`] | the 16.9 MB `whistle.cact` archive, validated |
//! | [`TranscribeOptions`] | language, keywords to bias towards, and word timestamps |
//! | [`Whistle`] | the speech model, held by exactly one value in the process |
//! | [`Transcript`] | one clip's text and [`Language`], with its [`Word`]s when asked for |
//! | [`Language`] | the seven languages Whistle transcribes |
//!
//! [`WhistleBuilder`] sets the options a [`Whistle`] transcribes with by default.
//!
//! Audio is 16 kHz mono `f32` PCM in `[-1, 1]` ([`SAMPLE_RATE`]), at most 30 seconds
//! ([`MAX_SAMPLES`]) per call. Clips are plain `&[f32]` slices, so the output of a WAV reader,
//! a resampler or a microphone callback goes in as it is. Every clip is checked before it reaches
//! the engine: a longer one is [`Error::AudioTooLong`], and a NaN or
//! an infinity is [`Error::NonFiniteSample`]. An empty clip is
//! not an error: it transcribes to an empty [`Transcript`], as silence does.
//!
//! [`Whistle::embed`] turns a clip into speech embeddings: one row of
//! [`Whistle::embedding_width`] floats (512 with the pinned weights) per 80 ms frame,
//! [`frame_count`] rows in all, flattened into one `Vec<f32>`.
//!
//! The speech model also runs inside the text model:
//! [`Needle::complete_audio`](crate::needle::Needle::complete_audio) transcribes a clip and
//! answers the transcript with tool calls in one engine call, once a [`Whistle`] has loaded the
//! speech weights.
//!
//! ## Usage
//!
//! ```no_run
//! use cactus_rs::whistle::{Language, TranscribeOptions, Weights, Whistle};
//!
//! let mut whistle = Whistle::builder(Weights::from_file("whistle.cact")?).build()?;
//!
//! // 16 kHz mono samples in [-1, 1], from a WAV reader or a microphone.
//! let pcm = vec![0.0_f32; 16_000];
//!
//! let transcript = whistle.transcribe(&pcm)?;
//! println!("{} ({})", transcript.text(), transcript.language_code());
//!
//! let options = TranscribeOptions::new()
//! .with_language(Language::En)
//! .with_keywords(["Siobhan", "Krzysztof"])
//! .with_word_timestamps(true);
//! for word in whistle.transcribe_with_options(&pcm, &options)?.words() {
//! println!("{:>6.2}s {}", word.start(), word.word());
//! }
//!
//! let embedding = whistle.embed(&pcm)?;
//! assert_eq!(
//! embedding.len(),
//! cactus_rs::whistle::frame_count(pcm.len()) * whistle.embedding_width()?
//! );
//! # Ok::<(), cactus_rs::Error>(())
//! ```
pub use ;
pub use ;
pub use ;
pub use Weights;
use crate;
/// The sample rate Whistle expects, in hertz. Clips are mono.
///
/// # Examples
///
/// ```rust
/// use cactus_rs::whistle::{MAX_SAMPLES, MAX_SECONDS, SAMPLE_RATE};
///
/// assert_eq!(SAMPLE_RATE as usize * MAX_SECONDS as usize, MAX_SAMPLES);
/// ```
pub const SAMPLE_RATE: u32 = 16_000;
/// The longest clip one call accepts, in seconds.
///
/// # Examples
///
/// ```rust
/// assert_eq!(cactus_rs::whistle::MAX_SECONDS, 30);
/// ```
pub const MAX_SECONDS: u32 = 30;
/// The longest clip one call accepts, in samples: 30 seconds at 16 kHz.
///
/// # Examples
///
/// ```rust
/// assert_eq!(cactus_rs::whistle::MAX_SAMPLES, 480_000);
/// ```
pub const MAX_SAMPLES: usize = crateMAX_SAMPLES;
/// The span of audio one speech embedding row covers, in milliseconds.
///
/// # Examples
///
/// ```rust
/// use cactus_rs::whistle::{FRAME_MS, FRAME_SAMPLES, SAMPLE_RATE};
///
/// assert_eq!(FRAME_SAMPLES, (SAMPLE_RATE / 1000 * FRAME_MS) as usize);
/// ```
pub const FRAME_MS: u32 = 80;
/// The span of audio one speech embedding row covers, in samples: 80 ms at 16 kHz.
///
/// # Examples
///
/// ```rust
/// assert_eq!(cactus_rs::whistle::FRAME_SAMPLES, 1_280);
/// ```
pub const FRAME_SAMPLES: usize = 1_280;
/// How many rows [`Whistle::embed`] returns for a clip of `samples` samples.
///
/// The engine frames audio with a 10 ms hop and pools eight hops into one 80 ms row, keeping a
/// trailing part-row once it holds more than one hop beyond the last full row. That works out
/// to `max(1, (samples + 1120) / 1280)`, measured against the pinned engine for every length
/// from 0 to [`MAX_SAMPLES`]: even an empty clip gets one row, and a full 30 s clip gets 375.
///
/// # Examples
///
/// ```rust
/// use cactus_rs::whistle::{MAX_SAMPLES, frame_count};
///
/// assert_eq!(frame_count(0), 1);
/// assert_eq!(frame_count(1_440), 2);
/// assert_eq!(frame_count(16_000), 13);
/// assert_eq!(frame_count(MAX_SAMPLES), 375);
/// ```
pub const
/// Checks a clip before it reaches the engine: length first, then every sample.
///
/// # Errors
///
/// Returns [`Error::AudioTooLong`] for more than [`MAX_SAMPLES`] samples, and
/// [`Error::NonFiniteSample`] naming the first NaN or infinity.
pub