diff --git a/Package.resolved b/Package.resolved index aa1387f..3c989e4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,48 +1,12 @@ { "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", - "version" : "5.9.1" - } - }, { "identity" : "eventsourcehttpbody", "kind" : "remoteSourceControl", "location" : "https://github.com/exyte/EventSourceHttpBody.git", "state" : { - "revision" : "9b68240460bae09faa0c5a9afbbccf5e18890e0c", - "version" : "0.1.3" - } - }, - { - "identity" : "moya", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Moya/Moya.git", - "state" : { - "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", - "version" : "15.0.3" - } - }, - { - "identity" : "reactiveswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", - "state" : { - "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", - "version" : "6.7.0" - } - }, - { - "identity" : "rxswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveX/RxSwift.git", - "state" : { - "revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a", - "version" : "6.7.1" + "revision" : "b000e62b83206dd6e00f2066cf08c96f232a4168", + "version" : "0.1.5" } } ], diff --git a/Sources/ExyteOpenAI/Endpoint Configurations/Audio.swift b/Sources/ExyteOpenAI/Endpoint Configurations/Audio.swift new file mode 100644 index 0000000..3534255 --- /dev/null +++ b/Sources/ExyteOpenAI/Endpoint Configurations/Audio.swift @@ -0,0 +1,142 @@ +// +// Audio.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +enum Audio { + case createTranscription(payload: CreateTranscriptionPayload) + case createTranslation(payload: CreateTranslationPayload) + case createSpeech(payload: CreateSpeechPayload, destination: URL) +} + +extension Audio: EndpointConfiguration { + + var method: HTTPRequestMethod { + return .post + } + + var path: String { + switch self { + case .createTranscription: + return "/audio/transcriptions" + case .createTranslation: + return "/audio/translations" + case .createSpeech: + return "/audio/speech" + } + } + + var task: RequestTask { + switch self { + case .createTranscription(let payload): + var data: [FormBodyPart] = [ + FormBodyPart( + name: "file", + value: .fileURL(payload.file), + fileName: payload.file.lastPathComponent, + mimeType: payload.file.pathExtension + ), + FormBodyPart( + name: "model", + value: .plainText(payload.model.rawValue) + ), + FormBodyPart( + name: "response_format", + value: .plainText(payload.responseFormat?.rawValue ?? TextResponseFormat.json.rawValue) + ) + ] + if let temperature = payload.temperature { + data.append( + FormBodyPart( + name: "temperature", + value: .floatingPoint(temperature) + ) + ) + } + if let prompt = payload.prompt { + data.append( + FormBodyPart( + name: "prompt", + value: .plainText(prompt) + ) + ) + } + if let language = payload.language { + data.append( + FormBodyPart( + name: "language", + value: .plainText(language)) + ) + } + if let timestampGranularities = payload.timestampGranularities, + payload.responseFormat == .verboseJson { + let timestampGranularitiesData = withUnsafeBytes(of: timestampGranularities) { Data($0) } + data.append( + FormBodyPart( + name: "timestamp_granularities", + value: .data(timestampGranularitiesData) + ) + ) + } + return .uploadMultipart(data) + case .createTranslation(let payload): + var data: [FormBodyPart] = [ + FormBodyPart( + name: "file", + value: .fileURL(payload.file), + fileName: payload.file.lastPathComponent, + mimeType: payload.file.pathExtension + ), + FormBodyPart( + name: "model", + value: .plainText(payload.model.rawValue) + ), + FormBodyPart( + name: "response_format", + value: .plainText(payload.responseFormat?.rawValue ?? TextResponseFormat.json.rawValue) + ) + ] + if let prompt = payload.prompt { + data.append( + FormBodyPart( + name: "prompt", + value: .plainText(prompt) + ) + ) + } + if let temperature = payload.temperature { + data.append( + FormBodyPart( + name: "temperature", + value: .floatingPoint(temperature) + ) + ) + } + return .uploadMultipart(data) + case .createSpeech(let payload, let destination): + return .download(payload, destination) + } + } + +} diff --git a/Sources/ExyteOpenAI/Endpoint Configurations/Files.swift b/Sources/ExyteOpenAI/Endpoint Configurations/Files.swift index b414cd0..09963f3 100644 --- a/Sources/ExyteOpenAI/Endpoint Configurations/Files.swift +++ b/Sources/ExyteOpenAI/Endpoint Configurations/Files.swift @@ -74,7 +74,7 @@ extension Files: EndpointConfiguration { case .listFiles, .retrieveFile, .deleteFile: return .plain case .retrieveFileContent(_, let destination): - return .download(destination) + return .download(nil, destination) } } diff --git a/Sources/ExyteOpenAI/Models/Audio/Transcription.swift b/Sources/ExyteOpenAI/Models/Audio/Transcription.swift new file mode 100644 index 0000000..5d85efb --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Audio/Transcription.swift @@ -0,0 +1,49 @@ +// +// Transcription.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct Transcription: Codable { + + let text: String + let language: String? + let duration: Double? + let words: String? + let segments: [TranscriptionSegment]? + + public init( + text: String, + language: String? = nil, + duration: Double? = nil, + words: String? = nil, + segments: [TranscriptionSegment]? = nil + ) { + self.text = text + self.language = language + self.duration = duration + self.words = words + self.segments = segments + } + +} diff --git a/Sources/ExyteOpenAI/Models/Audio/TranscriptionSegment.swift b/Sources/ExyteOpenAI/Models/Audio/TranscriptionSegment.swift new file mode 100644 index 0000000..cb789cf --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Audio/TranscriptionSegment.swift @@ -0,0 +1,40 @@ +// +// TranscriptionSegment.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct TranscriptionSegment: Codable { + + let id: Int + let seek: Int + let start: Double + let end: Double + let text: String + let tokens: [Int] + let temperature: Double + let avgLogprob: Double? + let compressionRatio: Double? + let noSpeechProb: Double? + +} diff --git a/Sources/ExyteOpenAI/Models/Audio/Translation.swift b/Sources/ExyteOpenAI/Models/Audio/Translation.swift new file mode 100644 index 0000000..911fdf3 --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Audio/Translation.swift @@ -0,0 +1,35 @@ +// +// Translation.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct Translation: Codable { + + let text: String + + public init(text: String) { + self.text = text + } + +} diff --git a/Sources/ExyteOpenAI/Models/Enums/AudioResponseFormat.swift b/Sources/ExyteOpenAI/Models/Enums/AudioResponseFormat.swift new file mode 100644 index 0000000..a717d8b --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/AudioResponseFormat.swift @@ -0,0 +1,36 @@ +// +// AudioResponseFormat.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum AudioResponseFormat: String, Codable { + + case mp3 = "mp3" + case opus = "opus" + case aac = "aac" + case flac = "flac" + case wav = "wav" + case pcm = "pcm" + +} diff --git a/Sources/ExyteOpenAI/Models/Enums/STTModel.swift b/Sources/ExyteOpenAI/Models/Enums/STTModel.swift new file mode 100644 index 0000000..7fe3491 --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/STTModel.swift @@ -0,0 +1,29 @@ +// +// STTModel.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum STTModel: String, Codable { + case whisper1 = "whisper-1" +} diff --git a/Sources/ExyteOpenAI/Models/Enums/SpeechVoice.swift b/Sources/ExyteOpenAI/Models/Enums/SpeechVoice.swift new file mode 100644 index 0000000..185795c --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/SpeechVoice.swift @@ -0,0 +1,36 @@ +// +// SpeechVoice.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum SpeechVoice: String, Codable { + + case alloy = "alloy" + case echo = "echo" + case fable = "fable" + case onyx = "onyx" + case nova = "nova" + case shimmer = "shimmer" + +} diff --git a/Sources/ExyteOpenAI/Models/Enums/TTSModel.swift b/Sources/ExyteOpenAI/Models/Enums/TTSModel.swift new file mode 100644 index 0000000..e557628 --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/TTSModel.swift @@ -0,0 +1,32 @@ +// +// TTSModel.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum TTSModel: String, Codable { + + case tts1 = "tts-1" + case tts1Hd = "tts-1-hd" + +} diff --git a/Sources/ExyteOpenAI/Models/Enums/TextResponseFormat.swift b/Sources/ExyteOpenAI/Models/Enums/TextResponseFormat.swift new file mode 100644 index 0000000..84f8560 --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/TextResponseFormat.swift @@ -0,0 +1,35 @@ +// +// TextResponseFormat.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum TextResponseFormat: String, Codable { + + case json = "json" + case text = "text" + case srt = "srt" + case verboseJson = "verbose_json" + case vtt = "vtt" + +} diff --git a/Sources/ExyteOpenAI/Models/Enums/TimestampGranularity.swift b/Sources/ExyteOpenAI/Models/Enums/TimestampGranularity.swift new file mode 100644 index 0000000..cfcab72 --- /dev/null +++ b/Sources/ExyteOpenAI/Models/Enums/TimestampGranularity.swift @@ -0,0 +1,30 @@ +// +// TimestampGranularity.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public enum TimestampGranularity: String, Codable { + case word = "word" + case segment = "segment" +} diff --git a/Sources/ExyteOpenAI/Networking/FormBodyPart.swift b/Sources/ExyteOpenAI/Networking/FormBodyPart.swift index 75f630d..5a7a559 100644 --- a/Sources/ExyteOpenAI/Networking/FormBodyPart.swift +++ b/Sources/ExyteOpenAI/Networking/FormBodyPart.swift @@ -30,6 +30,8 @@ public struct FormBodyPart { case fileURL(URL) case data(Data) case plainText(String) + case integer(Int) + case floatingPoint(Double) } public let name: String diff --git a/Sources/ExyteOpenAI/Networking/Provider.swift b/Sources/ExyteOpenAI/Networking/Provider.swift index 49401b9..37eca92 100644 --- a/Sources/ExyteOpenAI/Networking/Provider.swift +++ b/Sources/ExyteOpenAI/Networking/Provider.swift @@ -48,20 +48,29 @@ open class Provider { .eraseToAnyPublisher() } - open func downloadTaskPublisher(for endpoint: T) -> AnyPublisher { + open func downloadPublisher(for endpoint: T) -> AnyPublisher { + guard case let .download(_, destinationURL) = endpoint.task else { + return Fail(error: OpenAIError.incompatibleRequestTask) + .eraseToAnyPublisher() + } + return createRequestPublisher(for: endpoint) + .flatMap { [weak self] in + guard let self else { + return Fail(error: OpenAIError.requestCreationFailed) + .eraseToAnyPublisher() + } + return self.downloadTaskPublisher(for: $0, with: destinationURL) + } + .eraseToAnyPublisher() + } + + private func downloadTaskPublisher(for request: URLRequest, with destinationURL: URL) -> AnyPublisher { Future { [weak self] promise in guard let self else { promise(.failure(.requestCreationFailed)) return } - guard case let .download(destinationURL) = endpoint.task else { - promise(.failure(OpenAIError.incompatibleRequestTask)) - return - } - var urlRequest = URLRequest(url: OpenAI.baseURL.appending(path: endpoint.path)) - urlRequest.httpMethod = endpoint.method.rawValue - urlRequest.allHTTPHeaderFields = mandatoryHeaders.dictionary - URLSession.shared.downloadTask(with: urlRequest) { url, response, error in + URLSession.shared.downloadTask(with: request) { url, response, error in if let error { promise(.failure(OpenAIError.requestFailed(underlyingError: error))) return @@ -184,7 +193,21 @@ open class Provider { promise(.failure(OpenAIError.underlying(error))) } } - case .download: + case .download(let encodable, _): + if let encodable { + var headers = mandatoryHeaders + headers.append(.contentType(value: MimeType.json)) + urlRequest.allHTTPHeaderFields = headers.dictionary + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + do { + let data = try encoder.encode(encodable) + urlRequest.httpBody = data + promise(.success(urlRequest)) + } catch { + promise(.failure(.encodingFailed(underlyingError: error))) + } + } promise(.success(urlRequest)) } } @@ -234,6 +257,20 @@ open class Provider { throw OpenAIError.multipartEncoding(encodingError: .dataEncodingFailed) } bodyData = textData + case .floatingPoint(let value): + mimeType = $0.mimeType ?? MimeType.unknownBinary + if let numberData = "\(value)".data(using: .utf8) { + bodyData = numberData + } else { + throw OpenAIError.multipartEncoding(encodingError: .dataEncodingFailed) + } + case .integer(let value): + mimeType = $0.mimeType ?? MimeType.unknownBinary + if let numberData = "\(value)".data(using: .utf8) { + bodyData = numberData + } else { + throw OpenAIError.multipartEncoding(encodingError: .dataEncodingFailed) + } } body.append("Content-Type: \(mimeType)\(crlf)\(crlf)".data(using: String.Encoding.utf8)!) body.append(bodyData) diff --git a/Sources/ExyteOpenAI/Networking/RequestTask.swift b/Sources/ExyteOpenAI/Networking/RequestTask.swift index d28c56d..fc2b44b 100644 --- a/Sources/ExyteOpenAI/Networking/RequestTask.swift +++ b/Sources/ExyteOpenAI/Networking/RequestTask.swift @@ -29,5 +29,5 @@ public enum RequestTask { case JSONEncodable(Encodable) case URLParametersEncodable(Encodable) case uploadMultipart([FormBodyPart]) - case download(URL) + case download(Encodable?, URL) } diff --git a/Sources/ExyteOpenAI/OpenAI+Audio.swift b/Sources/ExyteOpenAI/OpenAI+Audio.swift new file mode 100644 index 0000000..be97afb --- /dev/null +++ b/Sources/ExyteOpenAI/OpenAI+Audio.swift @@ -0,0 +1,105 @@ +// +// OpenAI+Audio.swift +// +// Copyright (c) 2024 Exyte +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Combine + +// MARK: - Combine + +public extension OpenAI { + + func createSpeech(from payload: CreateSpeechPayload, destination: URL) -> AnyPublisher { + audioProvider.downloadPublisher(for: .createSpeech(payload: payload, destination: destination)) + .eraseToAnyPublisher() + } + + func createTranscription(from payload: CreateTranscriptionPayload) -> AnyPublisher { + audioProvider.requestPublisher(for: .createTranscription(payload: payload)) + .map { $0.data } + .map(to: Transcription.self, decoder: OpenAI.defaultDecoder) + .eraseToAnyPublisher() + } + + func createTranscription(from payload: CreateTranscriptionPayload) -> AnyPublisher { + audioProvider.requestPublisher(for: .createTranscription(payload: payload)) + .flatMap { + guard let stringData = String(data: $0.data, encoding: .utf8) else { + return Fail(error: .requestCreationFailed) + .eraseToAnyPublisher() + } + return Just(stringData) + .setFailureType(to: OpenAIError.self) + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + func createTranslation(from payload: CreateTranslationPayload) -> AnyPublisher { + audioProvider.requestPublisher(for: .createTranslation(payload: payload)) + .map { $0.data } + .map(to: Translation.self, decoder: OpenAI.defaultDecoder) + .eraseToAnyPublisher() + } + + func createTranslation(from payload: CreateTranslationPayload) -> AnyPublisher { + audioProvider.requestPublisher(for: .createTranslation(payload: payload)) + .flatMap { + guard let stringData = String(data: $0.data, encoding: .utf8) else { + return Fail(error: .requestCreationFailed) + .eraseToAnyPublisher() + } + return Just(stringData) + .setFailureType(to: OpenAIError.self) + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + +} + +// MARK: - Concurrency + +public extension OpenAI { + + func createSpeech(from payload: CreateSpeechPayload, destination: URL) async throws -> URL { + try await createSpeech(from: payload, destination: destination).async() + } + + func createTranscription(from payload: CreateTranscriptionPayload) async throws -> Transcription { + try await createTranscription(from: payload).async() + } + + func createTranscription(from payload: CreateTranscriptionPayload) async throws -> String { + try await createTranscription(from: payload).async() + } + + func createTranslation(from payload: CreateTranslationPayload) async throws -> Translation { + try await createTranslation(from: payload).async() + } + + func createTranslation(from payload: CreateTranslationPayload) async throws -> String { + try await createTranslation(from: payload).async() + } + +} diff --git a/Sources/ExyteOpenAI/OpenAI+Files.swift b/Sources/ExyteOpenAI/OpenAI+Files.swift index 7eb859f..743cd17 100644 --- a/Sources/ExyteOpenAI/OpenAI+Files.swift +++ b/Sources/ExyteOpenAI/OpenAI+Files.swift @@ -61,7 +61,7 @@ public extension OpenAI { /// You can’t download files that you’ve uploaded to the assistants yourself. /// If these files are important, you should consider storing a copy of these files before they’re uploaded. func retrieveFileContent(id: String, destinationURL: URL) -> AnyPublisher { - filesProvider.downloadTaskPublisher(for: .retrieveFileContent(id: id, destination: destinationURL)) + filesProvider.downloadPublisher(for: .retrieveFileContent(id: id, destination: destinationURL)) .eraseToAnyPublisher() } diff --git a/Sources/ExyteOpenAI/OpenAI.swift b/Sources/ExyteOpenAI/OpenAI.swift index 21814d6..a583995 100644 --- a/Sources/ExyteOpenAI/OpenAI.swift +++ b/Sources/ExyteOpenAI/OpenAI.swift @@ -47,6 +47,7 @@ public final class OpenAI { let runsProvider: Provider let filesProvider: Provider let chatsProvider: Provider + let audioProvider: Provider