| 1 | diff --git a/ios/transcription/AppleTranscriptionImpl.swift b/ios/transcription/AppleTranscriptionImpl.swift |
| 2 | index 188371a5f55fadae19187108c4a688569d0223e3..3e1cdd77b0bb8ae6c97a40d29c1854200d931eee 100644 |
| 3 | --- a/ios/transcription/AppleTranscriptionImpl.swift |
| 4 | +++ b/ios/transcription/AppleTranscriptionImpl.swift |
| 5 | @@ -12,6 +12,11 @@ import UniformTypeIdentifiers |
| 6 | |
| 7 | @objc |
| 8 | public class AppleTranscriptionImpl: NSObject { |
| 9 | + private struct CollectedSegment: Sendable { |
| 10 | + let text: String |
| 11 | + let startSecond: Double |
| 12 | + let endSecond: Double |
| 13 | + } |
| 14 | |
| 15 | @available(iOS 26, *) |
| 16 | private func createTranscriber(for locale: Locale) -> SpeechTranscriber { |
| 17 | @@ -41,7 +46,7 @@ public class AppleTranscriptionImpl: NSObject { |
| 18 | let locale = Locale(identifier: language) |
| 19 | |
| 20 | guard let supportedLocale = await SpeechTranscriber.supportedLocale(equivalentTo: locale) else { |
| 21 | - reject("AppleTranscription", "Locale not supported: \(language)", nil) |
| 22 | + reject("AppleTranscriptionUnsupportedLocale", "Locale not supported: \(language)", nil) |
| 23 | return |
| 24 | } |
| 25 | |
| 26 | @@ -51,16 +56,14 @@ public class AppleTranscriptionImpl: NSObject { |
| 27 | |
| 28 | switch status { |
| 29 | case .installed: |
| 30 | - resolve(nil) |
| 31 | + resolve(supportedLocale.identifier) |
| 32 | case .supported, .downloading: |
| 33 | - if let request = try? await AssetInventory.assetInstallationRequest(supporting: [transcriber]) { |
| 34 | + if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) { |
| 35 | try await request.downloadAndInstall() |
| 36 | - resolve(nil) |
| 37 | - } else { |
| 38 | - resolve(nil) |
| 39 | } |
| 40 | + resolve(supportedLocale.identifier) |
| 41 | case .unsupported: |
| 42 | - reject("AppleTranscription", "Assets not supported for locale: \(supportedLocale.identifier)", nil) |
| 43 | + reject("AppleTranscriptionUnsupportedLocale", "Assets not supported for locale: \(supportedLocale.identifier)", nil) |
| 44 | @unknown default: |
| 45 | reject ("AppleTranscription", "Unknown asset inventory status", nil) |
| 46 | } |
| 47 | @@ -83,57 +86,77 @@ public class AppleTranscriptionImpl: NSObject { |
| 48 | do { |
| 49 | try audioData.write(to: fileURL) |
| 50 | |
| 51 | - guard let audioFile = try? AVAudioFile(forReading: fileURL) else { |
| 52 | - reject("AppleTranscription", "Invalid audio data", nil) |
| 53 | + let audioFile: AVAudioFile |
| 54 | + do { |
| 55 | + audioFile = try AVAudioFile(forReading: fileURL) |
| 56 | + } catch { |
| 57 | + try? FileManager.default.removeItem(at: fileURL) |
| 58 | + reject("AppleTranscription", "Invalid audio data", error) |
| 59 | return |
| 60 | } |
| 61 | |
| 62 | Task { |
| 63 | + defer { |
| 64 | + try? FileManager.default.removeItem(at: fileURL) |
| 65 | + } |
| 66 | + |
| 67 | do { |
| 68 | let transcriber = createTranscriber(for: Locale(identifier: language)) |
| 69 | |
| 70 | let analyzer = SpeechAnalyzer(modules: [transcriber]) |
| 71 | - |
| 72 | - defer { |
| 73 | - try? FileManager.default.removeItem(at: fileURL) |
| 74 | - } |
| 75 | - |
| 76 | - var segments: [[String: Any]] = [] |
| 77 | - |
| 78 | - Task { |
| 79 | + |
| 80 | + let collectorTask = Task { () throws -> [CollectedSegment] in |
| 81 | + var segments: [CollectedSegment] = [] |
| 82 | + |
| 83 | for try await result in transcriber.results { |
| 84 | if result.isFinal { |
| 85 | - let segment: [String: Any] = [ |
| 86 | - "text": String(result.text.characters), |
| 87 | - "startSecond": CMTimeGetSeconds(result.range.start), |
| 88 | - "endSecond": CMTimeGetSeconds(CMTimeRangeGetEnd(result.range)) |
| 89 | - ] |
| 90 | - segments.append(segment) |
| 91 | + segments.append( |
| 92 | + CollectedSegment( |
| 93 | + text: String(result.text.characters), |
| 94 | + startSecond: CMTimeGetSeconds(result.range.start), |
| 95 | + endSecond: CMTimeGetSeconds(CMTimeRangeGetEnd(result.range)) |
| 96 | + ) |
| 97 | + ) |
| 98 | } |
| 99 | } |
| 100 | + |
| 101 | + return segments |
| 102 | } |
| 103 | - |
| 104 | - let lastSampleTime = try await analyzer.analyzeSequence(from: audioFile) |
| 105 | - |
| 106 | - if let lastSampleTime { |
| 107 | - try await analyzer.finalizeAndFinish(through: lastSampleTime) |
| 108 | - } else { |
| 109 | + |
| 110 | + do { |
| 111 | + let lastSampleTime = try await analyzer.analyzeSequence(from: audioFile) |
| 112 | + |
| 113 | + if let lastSampleTime { |
| 114 | + try await analyzer.finalizeAndFinish(through: lastSampleTime) |
| 115 | + } else { |
| 116 | + await analyzer.cancelAndFinishNow() |
| 117 | + } |
| 118 | + |
| 119 | + let segments: [[String: Any]] = try await collectorTask.value.map { segment in |
| 120 | + [ |
| 121 | + "text": segment.text, |
| 122 | + "startSecond": segment.startSecond, |
| 123 | + "endSecond": segment.endSecond |
| 124 | + ] |
| 125 | + } |
| 126 | + let totalDuration = if let lastSampleTime { CMTimeGetSeconds(lastSampleTime) } else { 0.0 } |
| 127 | + |
| 128 | + resolve([ |
| 129 | + "segments": segments, |
| 130 | + "duration": totalDuration |
| 131 | + ]) |
| 132 | + } catch { |
| 133 | + collectorTask.cancel() |
| 134 | await analyzer.cancelAndFinishNow() |
| 135 | + _ = try? await collectorTask.value |
| 136 | + throw error |
| 137 | } |
| 138 | - |
| 139 | - let totalDuration = if let lastSampleTime { CMTimeGetSeconds(lastSampleTime) } else { 0.0 } |
| 140 | - |
| 141 | - let result: [String: Any] = [ |
| 142 | - "segments": segments, |
| 143 | - "duration": totalDuration |
| 144 | - ] |
| 145 | - |
| 146 | - resolve(result) |
| 147 | } catch { |
| 148 | reject("AppleTranscription", "Transcription failed: \(error.localizedDescription)", error) |
| 149 | } |
| 150 | } |
| 151 | } catch { |
| 152 | + try? FileManager.default.removeItem(at: fileURL) |
| 153 | reject("AppleTranscription", "Failed to write audio data: \(error.localizedDescription)", error) |
| 154 | } |
| 155 | } else { |
| 156 | @@ -141,4 +164,3 @@ public class AppleTranscriptionImpl: NSObject { |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | - |
| 161 | diff --git a/lib/typescript/NativeAppleTranscription.d.ts b/lib/typescript/NativeAppleTranscription.d.ts |
| 162 | index 985b6b3593a884c41d689346be9d73d86c55eae1..86e7936f23a8673d099f7fe18af9a31abae5a13a 100644 |
| 163 | --- a/lib/typescript/NativeAppleTranscription.d.ts |
| 164 | +++ b/lib/typescript/NativeAppleTranscription.d.ts |
| 165 | @@ -10,14 +10,14 @@ export interface TranscriptionResult { |
| 166 | } |
| 167 | export interface Spec extends TurboModule { |
| 168 | isAvailable(language: string): boolean; |
| 169 | - prepare(language: string): Promise<void>; |
| 170 | + prepare(language: string): Promise<string>; |
| 171 | } |
| 172 | declare global { |
| 173 | function __apple__llm__transcribe__(data: ArrayBufferLike, language: string): Promise<TranscriptionResult>; |
| 174 | } |
| 175 | declare const _default: { |
| 176 | transcribe: (data: ArrayBufferLike, language: string) => Promise<TranscriptionResult>; |
| 177 | - prepare: (language: string) => Promise<void>; |
| 178 | + prepare: (language: string) => Promise<string>; |
| 179 | isAvailable: (language: string) => boolean; |
| 180 | }; |
| 181 | export default _default; |
| 182 | diff --git a/src/NativeAppleTranscription.ts b/src/NativeAppleTranscription.ts |
| 183 | index 13332a0176000b60b6f6f043de681db55a7b4bef..5389fb4b3a53de48721c307d2145b6770624c852 100644 |
| 184 | --- a/src/NativeAppleTranscription.ts |
| 185 | +++ b/src/NativeAppleTranscription.ts |
| 186 | @@ -14,7 +14,7 @@ export interface TranscriptionResult { |
| 187 | |
| 188 | export interface Spec extends TurboModule { |
| 189 | isAvailable(language: string): boolean |
| 190 | - prepare(language: string): Promise<void> |
| 191 | + prepare(language: string): Promise<string> |
| 192 | } |
| 193 | |
| 194 | declare global { |