- Make TranscriptionService a plain Sendable class (not @Observable/@MainActor) - Request speech authorization in ContentView.onAppear via callback (no async) - Use @State pendingMemo + Task in View for transcription (Swift 6 safe) - Separate saveRecording() and startTranscription() to avoid data races Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
50 lines
1.3 KiB
Swift
50 lines
1.3 KiB
Swift
import Foundation
|
|
import Speech
|
|
|
|
final class TranscriptionService: Sendable {
|
|
func requestAuthorization() async -> SFSpeechRecognizerAuthorizationStatus {
|
|
await withCheckedContinuation { continuation in
|
|
SFSpeechRecognizer.requestAuthorization { status in
|
|
continuation.resume(returning: status)
|
|
}
|
|
}
|
|
}
|
|
|
|
func transcribe(audioURL: URL) async throws -> String {
|
|
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "de-DE"))
|
|
|
|
guard let recognizer, recognizer.isAvailable else {
|
|
throw TranscriptionError.recognizerUnavailable
|
|
}
|
|
|
|
let request = SFSpeechURLRecognitionRequest(url: audioURL)
|
|
request.requiresOnDeviceRecognition = true
|
|
request.shouldReportPartialResults = false
|
|
request.addsPunctuation = true
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
recognizer.recognitionTask(with: request) { result, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let result, result.isFinal {
|
|
continuation.resume(returning: result.bestTranscription.formattedString)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TranscriptionError: LocalizedError {
|
|
case recognizerUnavailable
|
|
case noResult
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .recognizerUnavailable:
|
|
String(localized: "transcription.error.unavailable")
|
|
case .noResult:
|
|
String(localized: "transcription.error.noResult")
|
|
}
|
|
}
|
|
}
|