@@ -53,10 +53,42 @@ class TranscriptionService {
5353 throw CancellationError ( )
5454 }
5555
56- do {
57- return try await transcribeAudio ( fileURL: fileURL)
58- } catch let urlError as URLError where urlError. code == . timedOut {
59- throw TranscriptionError . transcriptionTimedOut ( transcriptionTimeoutSeconds)
56+ let timeoutSeconds = transcriptionTimeoutSeconds
57+ let raceState = TranscriptionTimeoutRaceState ( )
58+
59+ return try await withTaskCancellationHandler {
60+ try await withCheckedThrowingContinuation { continuation in
61+ raceState. setContinuation ( continuation)
62+
63+ let transcriptionTask = Task { [ weak self] in
64+ do {
65+ guard let self else {
66+ throw TranscriptionError . transcriptionFailed ( " Transcription service deallocated " )
67+ }
68+ let result = try await self . transcribeAudio ( fileURL: fileURL)
69+ raceState. finish ( . success( result) )
70+ } catch {
71+ raceState. finish ( . failure( Self . transcriptionTimeoutErrorIfNeeded (
72+ error,
73+ timeoutSeconds: timeoutSeconds
74+ ) ) )
75+ }
76+ }
77+
78+ let timeoutTask = Task {
79+ do {
80+ try await Task . sleep ( nanoseconds: UInt64 ( timeoutSeconds * 1_000_000_000 ) )
81+ raceState. finish ( . failure( TranscriptionError . transcriptionTimedOut ( timeoutSeconds) ) )
82+ } catch is CancellationError {
83+ } catch {
84+ raceState. finish ( . failure( error) )
85+ }
86+ }
87+
88+ raceState. setTasks ( [ transcriptionTask, timeoutTask] )
89+ }
90+ } onCancel: {
91+ raceState. cancel ( )
6092 }
6193 }
6294
@@ -208,6 +240,16 @@ class TranscriptionService {
208240 }
209241 }
210242
243+ private static func transcriptionTimeoutErrorIfNeeded(
244+ _ error: Error ,
245+ timeoutSeconds: TimeInterval
246+ ) -> Error {
247+ if let urlError = error as? URLError , urlError. code == . timedOut {
248+ return TranscriptionError . transcriptionTimedOut ( timeoutSeconds)
249+ }
250+ return error
251+ }
252+
211253 private static func normalizedBaseURL( from baseURL: String ) throws -> URL {
212254 let trimmed = baseURL. trimmingCharacters ( in: . whitespacesAndNewlines)
213255 guard !trimmed. isEmpty else {
@@ -341,6 +383,65 @@ enum TranscriptionError: LocalizedError {
341383 }
342384}
343385
386+ private final class TranscriptionTimeoutRaceState {
387+ private let lock = NSLock ( )
388+ private var didFinish = false
389+ private var continuation : CheckedContinuation < String , Error > ?
390+ private var tasks : [ Task < Void , Never > ] = [ ]
391+
392+ func setContinuation( _ continuation: CheckedContinuation < String , Error > ) {
393+ lock. lock ( )
394+ if didFinish {
395+ lock. unlock ( )
396+ continuation. resume ( throwing: CancellationError ( ) )
397+ return
398+ }
399+
400+ self . continuation = continuation
401+ lock. unlock ( )
402+ }
403+
404+ func setTasks( _ tasks: [ Task < Void , Never > ] ) {
405+ lock. lock ( )
406+ if didFinish {
407+ lock. unlock ( )
408+ tasks. forEach { $0. cancel ( ) }
409+ return
410+ }
411+
412+ self . tasks = tasks
413+ lock. unlock ( )
414+ }
415+
416+ func finish( _ result: Result < String , Error > ) {
417+ lock. lock ( )
418+ guard !didFinish else {
419+ lock. unlock ( )
420+ return
421+ }
422+
423+ didFinish = true
424+ let continuation = self . continuation
425+ self . continuation = nil
426+ let tasks = self . tasks
427+ self . tasks = [ ]
428+ lock. unlock ( )
429+
430+ tasks. forEach { $0. cancel ( ) }
431+
432+ switch result {
433+ case . success( let value) :
434+ continuation? . resume ( returning: value)
435+ case . failure( let error) :
436+ continuation? . resume ( throwing: error)
437+ }
438+ }
439+
440+ func cancel( ) {
441+ finish ( . failure( CancellationError ( ) ) )
442+ }
443+ }
444+
344445private struct PreparedUploadAudio {
345446 let fileURL : URL
346447 let deleteOnCleanup : Bool
0 commit comments