1- //! Owns the background OS thread that drives `wisp_audiokit::Session` .
1+ //! Owns the background OS thread that drives the platform audio session .
22//!
3- //! The Swift side calls back into Rust from arbitrary audio threads, and
4- //! `Session::start()/ stop()` block while async work runs underneath. To
5- //! keep the GPUI main thread responsive we run the lifecycle on a worker
6- //! thread and surface everything as a stream of `Update`s the UI polls.
3+ //! On macOS this is the backend-neutral orchestrator facade over the Swift
4+ //! capture/transcription callbacks. Start/ stop block while async platform work
5+ //! runs underneath, so the lifecycle stays on a worker thread and surfaces
6+ //! everything as a stream of `Update`s the UI polls.
77
88use std:: path:: PathBuf ;
99use std:: sync:: mpsc:: { Receiver , RecvTimeoutError , Sender , TryRecvError , channel} ;
1010use std:: thread:: JoinHandle ;
1111use std:: time:: { Duration , Instant } ;
1212
1313use chrono:: { DateTime , Utc } ;
14- use wisp_audiokit:: { Event , Session , SessionConfig , SessionError } ;
14+ #[ cfg( target_os = "macos" ) ]
15+ use wisp_audiokit:: MacosSession as PlatformSession ;
16+ #[ cfg( not( target_os = "macos" ) ) ]
17+ use wisp_audiokit:: Session as PlatformSession ;
18+ use wisp_audiokit:: { Event , SessionConfig , SessionError } ;
1519use wisp_core:: SessionId ;
1620
1721/// How often the running session checks for UI commands (Stop / Shutdown)
@@ -45,18 +49,24 @@ pub enum Command {
4549
4650/// Updates the worker sends back to the UI.
4751pub enum Update {
48- /// `Session::start()` returned successfully and audio is flowing.
52+ /// The platform session started successfully and audio is flowing.
4953 Started ( SessionStart ) ,
5054 /// One transcription / log event from the session.
5155 Event { session_id : SessionId , event : Event } ,
52- /// `Session::stop()` returned; the session has been torn down.
56+ /// The platform session stopped and has been torn down.
5357 Stopped { session_id : SessionId } ,
5458 /// Audio startup failed after constructing a session. Any partial capture
5559 /// has been stopped and its flushed events precede this update.
5660 StartFailed {
5761 session_id : SessionId ,
5862 error : SessionError ,
5963 } ,
64+ /// Capture/transcription failed after start; platform cleanup has already
65+ /// completed and partial audio/transcript must be finalized.
66+ RuntimeFailed {
67+ session_id : SessionId ,
68+ error : SessionError ,
69+ } ,
6070 /// Session construction failed before capture could start.
6171 Error {
6272 session_id : SessionId ,
@@ -159,6 +169,7 @@ fn is_terminal_for(
159169 match update {
160170 Update :: Stopped { session_id }
161171 | Update :: StartFailed { session_id, .. }
172+ | Update :: RuntimeFailed { session_id, .. }
162173 | Update :: Error { session_id, .. } => * session_id == expected_session_id,
163174 Update :: Started ( _) | Update :: Event { .. } => false ,
164175 }
@@ -201,7 +212,7 @@ fn run_session(
201212 update_tx : & Sender < Update > ,
202213) {
203214 let session_id = session_start. session_id ;
204- let mut session = match Session :: new_with_config ( output_dir, config) {
215+ let mut session = match PlatformSession :: new_with_config ( output_dir, config) {
205216 Ok ( s) => s,
206217 Err ( e) => {
207218 let _ = update_tx. send ( Update :: Error {
@@ -245,34 +256,86 @@ fn run_session(
245256 session. set_microphone_muted ( muted) ;
246257 } ,
247258 Ok ( Command :: Shutdown ) | Err ( TryRecvError :: Disconnected ) => {
248- session. stop ( ) ;
249- let _ = update_tx. send ( Update :: Stopped { session_id } ) ;
259+ stop_and_publish ( & mut session, session_id, update_tx) ;
250260 return ;
251261 } ,
252262 Ok ( Command :: Start { .. } ) | Err ( TryRecvError :: Empty ) => { } ,
253263 }
254264 if let Some ( event) = session. recv_timeout ( CMD_POLL_INTERVAL ) {
265+ #[ cfg( target_os = "macos" ) ]
266+ let terminal_error = session. take_runtime_failure ( ) ;
255267 let _ = update_tx. send ( Update :: Event { session_id, event } ) ;
268+ #[ cfg( target_os = "macos" ) ]
269+ if let Some ( error) = terminal_error {
270+ publish_runtime_failure_after_drain (
271+ || session. try_recv ( ) ,
272+ session_id,
273+ error,
274+ update_tx,
275+ ) ;
276+ return ;
277+ }
256278 }
257279 }
258280
281+ stop_and_publish ( & mut session, session_id, update_tx) ;
282+ }
283+
284+ fn stop_and_publish (
285+ session : & mut PlatformSession ,
286+ session_id : SessionId ,
287+ update_tx : & Sender < Update > ,
288+ ) {
259289 session. stop ( ) ;
260290 // Drain whatever the analyzer flushed during stop().
261291 while let Some ( event) = session. try_recv ( ) {
262292 let _ = update_tx. send ( Update :: Event { session_id, event } ) ;
263293 }
294+ #[ cfg( target_os = "macos" ) ]
295+ if let Some ( error) = session. take_runtime_failure ( ) {
296+ let _ = update_tx. send ( Update :: RuntimeFailed { session_id, error } ) ;
297+ return ;
298+ }
264299 let _ = update_tx. send ( Update :: Stopped { session_id } ) ;
265300}
266301
302+ #[ cfg( target_os = "macos" ) ]
303+ fn publish_runtime_failure_after_drain (
304+ mut try_recv : impl FnMut ( ) -> Option < Event > ,
305+ session_id : SessionId ,
306+ error : SessionError ,
307+ update_tx : & Sender < Update > ,
308+ ) {
309+ // A strict transcriber failure performs graceful native cleanup before it
310+ // becomes terminal. SpeechAnalyzer may emit its last final while that
311+ // cleanup is running, so publish every flushed event before the terminal
312+ // update tells the UI to finalize persistence.
313+ while let Some ( event) = try_recv ( ) {
314+ let _ = update_tx. send ( Update :: Event { session_id, event } ) ;
315+ }
316+ let _ = update_tx. send ( Update :: RuntimeFailed { session_id, error } ) ;
317+ }
318+
267319fn is_transcript_result ( event : & Event ) -> bool {
268320 matches ! ( event, Event :: Result ( _) )
269321}
270322
271323#[ cfg( test) ]
272324mod tests {
325+ #[ cfg( target_os = "macos" ) ]
326+ use std:: collections:: VecDeque ;
327+ #[ cfg( target_os = "macos" ) ]
328+ use std:: sync:: mpsc:: channel;
329+
330+ #[ cfg( target_os = "macos" ) ]
331+ use wisp_audiokit:: SessionError ;
273332 use wisp_audiokit:: { Event , SessionResult , SourceLabel } ;
333+ #[ cfg( target_os = "macos" ) ]
334+ use wisp_core:: SessionId ;
274335
275336 use super :: is_transcript_result;
337+ #[ cfg( target_os = "macos" ) ]
338+ use super :: { Update , publish_runtime_failure_after_drain} ;
276339
277340 #[ test]
278341 fn only_transcript_results_require_preserving_a_failed_start ( ) {
@@ -288,4 +351,44 @@ mod tests {
288351 confidence_min: None ,
289352 } ) ) ) ;
290353 }
354+
355+ #[ cfg( target_os = "macos" ) ]
356+ #[ test]
357+ fn runtime_failure_publishes_final_flushed_during_cleanup_first ( ) {
358+ let session_id = SessionId :: from ( 42 ) ;
359+ let final_result = Event :: Result ( SessionResult {
360+ source : SourceLabel :: System ,
361+ segment_id : 42 ,
362+ is_final : true ,
363+ text : "cleanup final" . into ( ) ,
364+ start_seconds : 2.0 ,
365+ end_seconds : 3.0 ,
366+ confidence_mean : Some ( 0.9 ) ,
367+ confidence_min : Some ( 0.8 ) ,
368+ } ) ;
369+ let mut cleanup_events = VecDeque :: from ( [ final_result. clone ( ) ] ) ;
370+ let ( tx, rx) = channel ( ) ;
371+
372+ publish_runtime_failure_after_drain (
373+ || cleanup_events. pop_front ( ) ,
374+ session_id,
375+ SessionError :: Start ( "strict transcriber failure" . into ( ) ) ,
376+ & tx,
377+ ) ;
378+
379+ assert ! ( matches!(
380+ rx. recv( ) . unwrap( ) ,
381+ Update :: Event {
382+ session_id: actual,
383+ event,
384+ } if actual == session_id && event == final_result
385+ ) ) ;
386+ assert ! ( matches!(
387+ rx. recv( ) . unwrap( ) ,
388+ Update :: RuntimeFailed {
389+ session_id: actual,
390+ ..
391+ } if actual == session_id
392+ ) ) ;
393+ }
291394}
0 commit comments