136 lines
4.9 KiB
Swift
136 lines
4.9 KiB
Swift
import Foundation
|
|
|
|
enum TranscriptionError: LocalizedError {
|
|
case noFile
|
|
case notConfigured
|
|
case networkError(String)
|
|
case apiError(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noFile:
|
|
return "Keine Audio-Datei gefunden"
|
|
case .notConfigured:
|
|
return "OpenAI API Key fehlt. Bitte in den Einstellungen hinterlegen."
|
|
case .networkError(let msg):
|
|
return "Netzwerkfehler: \(msg)"
|
|
case .apiError(let msg):
|
|
return "OpenAI-Fehler: \(msg)"
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct TranscriptionOpenAIErrorResponse: Decodable {
|
|
struct APIError: Decodable {
|
|
let message: String?
|
|
}
|
|
|
|
let error: APIError?
|
|
}
|
|
|
|
enum TranscriptionService {
|
|
private static let remoteModel = "whisper-1"
|
|
private static let transcriptionsURL = URL(string: "https://api.openai.com/v1/audio/transcriptions")!
|
|
|
|
private static let session: URLSession = {
|
|
let configuration = URLSessionConfiguration.ephemeral
|
|
configuration.waitsForConnectivity = false
|
|
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
configuration.timeoutIntervalForRequest = 60
|
|
configuration.timeoutIntervalForResource = 60
|
|
return URLSession(configuration: configuration)
|
|
}()
|
|
|
|
static func transcribe(
|
|
audioURL: URL,
|
|
customTerms: [String] = [],
|
|
language: String? = nil
|
|
) async throws -> String {
|
|
guard let apiKey = KeychainService.load(key: .openAIAPIKey) else {
|
|
throw TranscriptionError.notConfigured
|
|
}
|
|
|
|
return try await Task.detached(priority: .userInitiated) {
|
|
defer {
|
|
try? FileManager.default.removeItem(at: audioURL)
|
|
}
|
|
|
|
let boundary = UUID().uuidString
|
|
var request = URLRequest(url: transcriptionsURL)
|
|
request.httpMethod = "POST"
|
|
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("text/plain, application/json", forHTTPHeaderField: "Accept")
|
|
request.timeoutInterval = 60
|
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
|
|
let audioData = try Data(contentsOf: audioURL, options: [.mappedIfSafe])
|
|
|
|
var body = Data()
|
|
body.append("--\(boundary)\r\n")
|
|
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"audio.m4a\"\r\n")
|
|
body.append("Content-Type: audio/m4a\r\n\r\n")
|
|
body.append(audioData)
|
|
body.append("\r\n")
|
|
|
|
body.append("--\(boundary)\r\n")
|
|
body.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n")
|
|
body.append(remoteModel)
|
|
body.append("\r\n")
|
|
|
|
body.append("--\(boundary)\r\n")
|
|
body.append("Content-Disposition: form-data; name=\"response_format\"\r\n\r\n")
|
|
body.append("text")
|
|
body.append("\r\n")
|
|
|
|
if !customTerms.isEmpty {
|
|
let prompt = "Eigennamen und Begriffe: \(customTerms.joined(separator: ", "))"
|
|
body.append("--\(boundary)\r\n")
|
|
body.append("Content-Disposition: form-data; name=\"prompt\"\r\n\r\n")
|
|
body.append(prompt)
|
|
body.append("\r\n")
|
|
}
|
|
|
|
if let language, !language.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
body.append("--\(boundary)\r\n")
|
|
body.append("Content-Disposition: form-data; name=\"language\"\r\n\r\n")
|
|
body.append(language.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
body.append("\r\n")
|
|
}
|
|
|
|
body.append("--\(boundary)--\r\n")
|
|
request.httpBody = body
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw TranscriptionError.networkError("Ungueltige Antwort")
|
|
}
|
|
|
|
guard httpResponse.statusCode == 200 else {
|
|
throw TranscriptionError.apiError(openAIErrorMessage(from: data) ?? "Status \(httpResponse.statusCode)")
|
|
}
|
|
|
|
guard let text = String(data: data, encoding: .utf8)?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!text.isEmpty else {
|
|
throw TranscriptionError.apiError("Transkription fehlgeschlagen")
|
|
}
|
|
|
|
return text
|
|
}.value
|
|
}
|
|
|
|
private static func openAIErrorMessage(from data: Data) -> String? {
|
|
(try? JSONDecoder().decode(TranscriptionOpenAIErrorResponse.self, from: data))?.error?.message
|
|
}
|
|
}
|
|
|
|
private extension Data {
|
|
mutating func append(_ string: String) {
|
|
if let data = string.data(using: .utf8) {
|
|
append(data)
|
|
}
|
|
}
|
|
}
|