From 88a4c19f7d2810fa0536641bd82a4b0268555147 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 14:52:02 -0700 Subject: [PATCH 01/73] Initial --- .../Intramodular/ElevenLabs.Client.swift | 500 ++++++++---------- 1 file changed, 235 insertions(+), 265 deletions(-) diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index dd59c3eb..acd45418 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -7,299 +7,269 @@ import CorePersistence import Foundation import NetworkKit -extension ElevenLabs { - @RuntimeDiscoverable - public final class Client: ObservableObject { - public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._ElevenLabs - } - - public struct Configuration { - public var apiKey: String? - } - - public let configuration: Configuration - public let apiSpecification = APISpecification() - - public required init( - configuration: Configuration - ) { - self.configuration = configuration - } - - public convenience init( - apiKey: String? - ) { - self.init(configuration: .init(apiKey: apiKey)) - } +public struct ElevenLabsAPI: RESTAPISpecification { + public var apiKey: String + public var host = URL(string: "https://api.elevenlabs.io")! + + public init(apiKey: String) { + self.apiKey = apiKey } -} - -extension ElevenLabs.Client: _MIService { - public convenience init( - account: (any _MIServiceAccount)? - ) async throws { - let account = try account.unwrap() - - guard account.serviceIdentifier == _MIServiceTypeIdentifier._ElevenLabs else { - throw _MIServiceError.serviceTypeIncompatible(account.serviceIdentifier) - } - - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) - } - - self.init(apiKey: credential.apiKey) + + public var baseURL: URL { + host.appendingPathComponent("/v1") + } + + public var id: some Hashable { + apiKey } + + // List Voices endpoint + @Path("voices") + @GET + var listVoices = Endpoint() + + // Text to Speech endpoint + @Path("text-to-speech/{voiceId}") + @POST + @Body({ context in + RequestBodies.SpeechRequest( + text: context.input.text, + voiceSettings: context.input.voiceSettings, + model: context.input.model + ) + }) + var textToSpeech = Endpoint() + + // Speech to Speech endpoint + @Path("speech-to-speech/{voiceId}/stream") + @POST + var speechToSpeech = MultipartEndpoint() + + // Add Voice endpoint + @Path("voices/add") + @POST + var addVoice = MultipartEndpoint() + + // Edit Voice endpoint + @Path("voices/{voiceId}/edit") + @POST + var editVoice = MultipartEndpoint() + + // Delete Voice endpoint + @Path("voices/{voiceId}") + @DELETE + var deleteVoice = Endpoint() } -extension ElevenLabs.Client { - public func availableVoices() async throws -> [ElevenLabs.Voice] { - let request = HTTPRequest(url: URL(string: "\(apiSpecification.host)/v1/voices")!) - .method(.get) - .header("xi-api-key", configuration.apiKey) - .header(.contentType(.json)) - - let response = try await HTTPSession.shared.data(for: request) - - try response.validate() +extension ElevenLabsAPI { + public final class Endpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + let request = try super.buildRequestBase(from: input, context: context) + .header("xi-api-key", context.root.apiKey) + .header(.contentType(.json)) + + return request + } - return try response.decode( - ElevenLabs.APISpecification.ResponseBodies.Voices.self, - keyDecodingStrategy: .convertFromSnakeCase - ) - .voices - } - - @discardableResult - public func speech( - for text: String, - voiceID: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) async throws -> Data { - let request = try HTTPRequest(url: URL(string: "\(apiSpecification.host)/v1/text-to-speech/\(voiceID)")!) - .method(.post) - .header("xi-api-key", configuration.apiKey) - .header(.contentType(.json)) - .header(.accept(.mpeg)) - .jsonBody( - ElevenLabs.APISpecification.RequestBodies.SpeechRequest( - text: text, - voiceSettings: voiceSettings, - model: model - ), - keyEncodingStrategy: .convertToSnakeCase + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + try response.validate() + + if Output.self == Data.self { + return response.data as! Output + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase ) - - let response = try await HTTPSession.shared.data(for: request) - - try response.validate() - - return response.data + } } - public func speechToSpeech( - inputAudioURL: URL, - voiceID: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) async throws -> Data { - let boundary = UUID().uuidString - - var request = try URLRequest(url: URL(string: "\(apiSpecification.host)/v1/speech-to-speech/\(voiceID)/stream").unwrap()) - - request.httpMethod = "POST" - request.setValue("audio/mpeg", forHTTPHeaderField: "accept") - request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key") - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - var data = Data() - - // Add model_id - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"model_id\"\r\n\r\n".data(using: .utf8)!) - data.append("\(model.rawValue)\r\n".data(using: .utf8)!) - - // Add voice settings - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - let voiceSettingsData = try encoder.encode(voiceSettings) - let voiceSettingsString = String(data: voiceSettingsData, encoding: .utf8)! - - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"voice_settings\"\r\n\r\n".data(using: .utf8)!) - data.append("\(voiceSettingsString)\r\n".data(using: .utf8)!) - - // Add audio file - if let fileData = createMultipartData(boundary: boundary, name: "audio", fileURL: inputAudioURL, fileType: "audio/mpeg") { - data.append(fileData) + public final class MultipartEndpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + let boundary = UUID().uuidString + var request = try super.buildRequestBase(from: input, context: context) + .header("xi-api-key", context.root.apiKey) + .header(.contentType(.multipartFormData(boundary: boundary))) + + var data = Data() + + switch input { + case let input as RequestBodies.SpeechToSpeechInput: + data.append(input.createMultipartFormData(boundary: boundary)) + case let input as RequestBodies.AddVoiceInput: + data.append(input.createMultipartFormData(boundary: boundary)) + case let input as RequestBodies.EditVoiceInput: + data.append(input.createMultipartFormData(boundary: boundary)) + default: + throw Never.Reason.unexpected + } + + request.httpBody = data + + return request } - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - - request.httpBody = data - - return try await withUnsafeThrowingContinuation { continuation in - let task = URLSession.shared.dataTask(with: request) { (data, response, error) in - if let error = error { - continuation.resume(throwing: error) - } else if let data = data { - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { - continuation.resume(returning: data) - } else { - continuation.resume(throwing: _PlaceholderError()) - } - } + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + try response.validate() + + if Output.self == Data.self { + return response.data as! Output } - task.resume() + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) } } - - public func upload( - voiceWithName name: String, - description: String, - fileURL: URL - ) async throws -> ElevenLabs.Voice.ID { - let boundary = UUID().uuidString - - var request = try URLRequest(url: URL(string: "\(apiSpecification.host)/v1/voices/add").unwrap()) - - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "accept") - request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key") - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - var data = Data() - let parameters = [ - ("name", name), - ("description", description), - ("labels", "") - ] - - for (key, value) in parameters { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - data.append("\(value)\r\n".data(using: .utf8)!) +} + +// Request and Response Bodies +extension ElevenLabsAPI { + public enum RequestBodies { + public struct SpeechRequest: Codable { + let text: String + let voiceSettings: ElevenLabs.VoiceSettings + let model: ElevenLabs.Model } - if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") { - data.append(fileData) + public struct TextToSpeechInput { + let voiceId: String + let text: String + let voiceSettings: ElevenLabs.VoiceSettings + let model: ElevenLabs.Model } - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - - request.httpBody = data - - let voiceID: String? = try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - let task = URLSession.shared.dataTask(with: request) { (data, response, error) in - if let error = error { - continuation.resume(throwing: error) - } else if let data = data { - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - guard let voiceID: String = json["voice_id"] as? String else { return } - continuation.resume(returning: voiceID) - } else { - continuation.resume(returning: nil) - } - } catch { - continuation.resume(throwing: _PlaceholderError()) - } - } else { - continuation.resume(throwing: _PlaceholderError()) - } + public struct SpeechToSpeechInput { + let voiceId: String + let audioURL: URL + let voiceSettings: ElevenLabs.VoiceSettings + let model: ElevenLabs.Model + + func createMultipartFormData(boundary: String) -> Data { + var data = Data() + + // Add model_id + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"model_id\"\r\n\r\n".data(using: .utf8)!) + data.append("\(model.rawValue)\r\n".data(using: .utf8)!) + + // Add voice settings + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String(data: voiceSettingsData, encoding: .utf8) { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"voice_settings\"\r\n\r\n".data(using: .utf8)!) + data.append("\(voiceSettingsString)\r\n".data(using: .utf8)!) + } + + // Add audio file + if let fileData = try? Data(contentsOf: audioURL) { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"audio\"; filename=\"\(audioURL.lastPathComponent)\"\r\n".data(using: .utf8)!) + data.append("Content-Type: audio/mpeg\r\n\r\n".data(using: .utf8)!) + data.append(fileData) + data.append("\r\n".data(using: .utf8)!) } + + data.append("--\(boundary)--\r\n".data(using: .utf8)!) + return data } - - task.resume() } - return try .init(rawValue: voiceID.unwrap()) - } - - public func edit( - voice: ElevenLabs.Voice.ID, - name: String, - description: String, - fileURL: URL - ) async throws -> Bool { - let url = URL(string: "\(apiSpecification.host)/v1/voices/\(voice.rawValue)/edit")! - - let boundary = UUID().uuidString - var request = URLRequest(url: url) - - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "accept") - request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key") - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - var data = Data() - let parameters = [ - ("name", name), - ("description", description), - ("labels", "") - ] - - for (key, value) in parameters { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - data.append("\(value)\r\n".data(using: .utf8)!) + public struct AddVoiceInput { + let name: String + let description: String + let fileURL: URL + + func createMultipartFormData(boundary: String) -> Data { + var data = Data() + + // Add name and description + let parameters = [ + ("name", name), + ("description", description), + ("labels", "") + ] + + for (key, value) in parameters { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) + data.append("\(value)\r\n".data(using: .utf8)!) + } + + // Add audio file + if let fileData = try? Data(contentsOf: fileURL) { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(fileURL.lastPathComponent)\"\r\n".data(using: .utf8)!) + data.append("Content-Type: audio/x-wav\r\n\r\n".data(using: .utf8)!) + data.append(fileData) + data.append("\r\n".data(using: .utf8)!) + } + + data.append("--\(boundary)--\r\n".data(using: .utf8)!) + return data + } } - if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") { - data.append(fileData) + public struct EditVoiceInput { + let voiceId: String + let name: String + let description: String + let fileURL: URL + + func createMultipartFormData(boundary: String) -> Data { + var data = Data() + + // Add name and description + let parameters = [ + ("name", name), + ("description", description), + ("labels", "") + ] + + for (key, value) in parameters { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) + data.append("\(value)\r\n".data(using: .utf8)!) + } + + // Add audio file + if let fileData = try? Data(contentsOf: fileURL) { + data.append("--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(fileURL.lastPathComponent)\"\r\n".data(using: .utf8)!) + data.append("Content-Type: audio/x-wav\r\n\r\n".data(using: .utf8)!) + data.append(fileData) + data.append("\r\n".data(using: .utf8)!) + } + + data.append("--\(boundary)--\r\n".data(using: .utf8)!) + return data + } } - - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - - request.httpBody = data - - let response = try await HTTPSession.shared.data(for: request) - - try response.validate() - - return true - } - - public func delete( - voice: ElevenLabs.Voice.ID - ) async throws { - let url = try URL(string: "\(apiSpecification.host)/v1/voices/\(voice.rawValue)").unwrap() - - var request = URLRequest(url: url) - - request.httpMethod = "DELETE" - request.setValue("application/json", forHTTPHeaderField: "accept") - request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key") - - let response = try await HTTPSession.shared.data(for: request) - - try response.validate() } - private func createMultipartData( - boundary: String, - name: String, - fileURL: URL, - fileType: String - ) -> Data? { - var result = Data() - let fileName = fileURL.lastPathComponent - - result.append("--\(boundary)\r\n".data(using: .utf8)!) - result.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) - result.append("Content-Type: \(fileType)\r\n\r\n".data(using: .utf8)!) - - guard let fileData = try? Data(contentsOf: fileURL) else { - return nil + public enum ResponseBodies { + public struct Voices: Codable { + public let voices: [ElevenLabs.Voice] } - result.append(fileData) - result.append("\r\n".data(using: .utf8)!) - - return result + public struct VoiceID: Codable { + public let voiceId: String + } } } From 504d5067b78cd9d6c0eee86b0b3c1a0782c6c141 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 15:59:50 -0700 Subject: [PATCH 02/73] Updates to Endpoints & RequestBodies --- .../ElevenLabs.APISpecification.swift | 185 ++++++-- .../Intramodular/ElevenLabs.Client.swift | 428 +++++++++--------- .../ElevenLabs.VoiceSettings.swift | 10 +- 3 files changed, 375 insertions(+), 248 deletions(-) diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift index 9e0c2a26..4b55c824 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift @@ -3,50 +3,183 @@ // import CorePersistence -import Swallow +import Diagnostics +import NetworkKit +import Swift +import SwiftAPI extension ElevenLabs { - public struct APISpecification { + public enum APIError: APIErrorProtocol { + public typealias API = ElevenLabs.APISpecification + + case apiKeyMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case invalidContentType + case badRequest(request: API.Request?, error: API.Request.Error) + case unknown(message: String) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var host: URL + public var apiKey: String? + + public init( + host: URL = URL(string: "https://api.elevenlabs.io")!, + apiKey: String? = nil + ) { + self.host = host + self.apiKey = apiKey + } + } + + public let configuration: Configuration + public var host: URL { - URL(string: "https://api.elevenlabs.io")! + configuration.host + } + + public var id: some Hashable { + configuration + } + + public init(configuration: Configuration) { + self.configuration = configuration } + + // Voices endpoints + @GET + @Path("/v1/voices") + var listVoices = Endpoint() + + // Text to speech + @POST + @Path({ context -> String in + "/v1/text-to-speech/\(context.input.voiceId)" + }) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) + var textToSpeech = Endpoint() + + // Speech to speech + @POST + @Path({ context -> String in + "/v1/speech-to-speech/\(context.input.voiceId)/stream" + }) + @Body(multipart: .input) + var speechToSpeech = Endpoint() + + // Voice management + @POST + @Path("/v1/voices/add") + @Body(multipart: .input) + var addVoice = Endpoint() + + @POST + @Path({ context -> String in + "/v1/voices/\(context.input.voiceId)/edit" + }) + @Body(multipart: .input) + var editVoice = Endpoint() + + @DELETE + @Path({ context -> String in + "/v1/voices/\(context.input)" + }) + var deleteVoice = Endpoint() } } extension ElevenLabs.APISpecification { - enum RequestBodies { - public struct SpeechRequest: Codable { - public enum CodingKeys: String, CodingKey { - case text - case voiceSettings - case model - } + public final class Endpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + var request = try super.buildRequestBase( + from: input, + context: context + ) - let text: String - let voiceSettings: ElevenLabs.VoiceSettings - let model: ElevenLabs.Model + request = request.header("xi-api-key", context.root.configuration.apiKey) + .header(.contentType(.json)) - init( - text: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) { - self.text = text - self.voiceSettings = voiceSettings - self.model = model + return request + } + + struct _ErrorWrapper: Codable, Hashable { + let detail: ErrorDetail + + struct ErrorDetail: Codable, Hashable { + let status: String + let message: String + } + } + + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? HTTPRequest.Error { + let errorWrapper = try? response.decode( + _ErrorWrapper.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + + if let message = errorWrapper?.detail.message { + if message.contains("API key is missing") { + throw Error.apiKeyMissing + } else if message.contains("Invalid API key") { + throw Error.incorrectAPIKeyProvided + } else { + throw Error.unknown(message: message) + } + } + + if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + if Output.self == Data.self { + return response.data as! Output } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) } } } extension ElevenLabs.APISpecification { public enum ResponseBodies { - public struct Voices: Codable, Hashable, Sendable { + public struct Voices: Codable { public let voices: [ElevenLabs.Voice] - - public init(voices: [ElevenLabs.Voice]) { - self.voices = voices - } + } + + public struct VoiceID: Codable { + public let voiceId: String } } } diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index acd45418..c1a722df 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -2,274 +2,268 @@ // Copyright (c) Vatsal Manot // -import CoreMI import CorePersistence -import Foundation +import Diagnostics import NetworkKit +import Foundation +import SwiftAPI +import Merge +import FoundationX +import Swallow -public struct ElevenLabsAPI: RESTAPISpecification { - public var apiKey: String - public var host = URL(string: "https://api.elevenlabs.io")! - - public init(apiKey: String) { - self.apiKey = apiKey - } - - public var baseURL: URL { - host.appendingPathComponent("/v1") - } - - public var id: some Hashable { - apiKey - } - - // List Voices endpoint - @Path("voices") - @GET - var listVoices = Endpoint() - - // Text to Speech endpoint - @Path("text-to-speech/{voiceId}") - @POST - @Body({ context in - RequestBodies.SpeechRequest( - text: context.input.text, - voiceSettings: context.input.voiceSettings, - model: context.input.model - ) - }) - var textToSpeech = Endpoint() - - // Speech to Speech endpoint - @Path("speech-to-speech/{voiceId}/stream") - @POST - var speechToSpeech = MultipartEndpoint() - - // Add Voice endpoint - @Path("voices/add") - @POST - var addVoice = MultipartEndpoint() - - // Edit Voice endpoint - @Path("voices/{voiceId}/edit") - @POST - var editVoice = MultipartEndpoint() - - // Delete Voice endpoint - @Path("voices/{voiceId}") - @DELETE - var deleteVoice = Endpoint() -} - -extension ElevenLabsAPI { - public final class Endpoint: BaseHTTPEndpoint { - override public func buildRequestBase( - from input: Input, - context: BuildRequestContext - ) throws -> Request { - let request = try super.buildRequestBase(from: input, context: context) - .header("xi-api-key", context.root.apiKey) - .header(.contentType(.json)) - - return request +extension ElevenLabs { + @RuntimeDiscoverable + public final class Client: SwiftAPI.Client, ObservableObject { + public typealias API = ElevenLabs.APISpecification + public typealias Session = HTTPSession + + public let interface: API + public let session: Session + public var sessionCache: EmptyKeyedCache + + public required init(configuration: API.Configuration) { + self.interface = API(configuration: configuration) + self.session = HTTPSession.shared + self.sessionCache = .init() } - override public func decodeOutputBase( - from response: Request.Response, - context: DecodeOutputContext - ) throws -> Output { - try response.validate() - - if Output.self == Data.self { - return response.data as! Output - } - - return try response.decode( - Output.self, - keyDecodingStrategy: .convertFromSnakeCase - ) + public convenience init(apiKey: String?) { + self.init(configuration: .init(apiKey: apiKey)) } } +} + +extension ElevenLabs.APISpecification { - public final class MultipartEndpoint: BaseHTTPEndpoint { - override public func buildRequestBase( - from input: Input, - context: BuildRequestContext - ) throws -> Request { - let boundary = UUID().uuidString - var request = try super.buildRequestBase(from: input, context: context) - .header("xi-api-key", context.root.apiKey) - .header(.contentType(.multipartFormData(boundary: boundary))) + enum RequestBodies { + public struct SpeechRequest: Codable, Hashable, Equatable { + public let text: String + public let voiceSettings: ElevenLabs.VoiceSettings + public let model: ElevenLabs.Model - var data = Data() - - switch input { - case let input as RequestBodies.SpeechToSpeechInput: - data.append(input.createMultipartFormData(boundary: boundary)) - case let input as RequestBodies.AddVoiceInput: - data.append(input.createMultipartFormData(boundary: boundary)) - case let input as RequestBodies.EditVoiceInput: - data.append(input.createMultipartFormData(boundary: boundary)) - default: - throw Never.Reason.unexpected + public init( + text: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) { + self.text = text + self.voiceSettings = voiceSettings + self.model = model } - request.httpBody = data - - return request + public static func == (lhs: SpeechRequest, rhs: SpeechRequest) -> Bool { + return lhs.text == rhs.text && + lhs.voiceSettings == rhs.voiceSettings && + lhs.model == rhs.model + } } - override public func decodeOutputBase( - from response: Request.Response, - context: DecodeOutputContext - ) throws -> Output { - try response.validate() + public struct TextToSpeechInput: Codable, Hashable { + public let voiceId: String + public let requestBody: SpeechRequest - if Output.self == Data.self { - return response.data as! Output + public init(voiceId: String, requestBody: SpeechRequest) { + self.voiceId = voiceId + self.requestBody = requestBody } - - return try response.decode( - Output.self, - keyDecodingStrategy: .convertFromSnakeCase - ) - } - } -} - -// Request and Response Bodies -extension ElevenLabsAPI { - public enum RequestBodies { - public struct SpeechRequest: Codable { - let text: String - let voiceSettings: ElevenLabs.VoiceSettings - let model: ElevenLabs.Model } - public struct TextToSpeechInput { - let voiceId: String - let text: String - let voiceSettings: ElevenLabs.VoiceSettings - let model: ElevenLabs.Model - } - - public struct SpeechToSpeechInput { - let voiceId: String - let audioURL: URL - let voiceSettings: ElevenLabs.VoiceSettings - let model: ElevenLabs.Model + public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let audioURL: URL + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings - func createMultipartFormData(boundary: String) -> Data { - var data = Data() + public init( + voiceId: String, + audioURL: URL, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.model = model + self.voiceSettings = voiceSettings + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - // Add model_id - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"model_id\"\r\n\r\n".data(using: .utf8)!) - data.append("\(model.rawValue)\r\n".data(using: .utf8)!) + result.append(.text(named: "model_id", value: model.rawValue)) - // Add voice settings let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase if let voiceSettingsData = try? encoder.encode(voiceSettings), let voiceSettingsString = String(data: voiceSettingsData, encoding: .utf8) { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"voice_settings\"\r\n\r\n".data(using: .utf8)!) - data.append("\(voiceSettingsString)\r\n".data(using: .utf8)!) + result.append(.text(named: "voice_settings", value: voiceSettingsString)) } - // Add audio file if let fileData = try? Data(contentsOf: audioURL) { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"audio\"; filename=\"\(audioURL.lastPathComponent)\"\r\n".data(using: .utf8)!) - data.append("Content-Type: audio/mpeg\r\n\r\n".data(using: .utf8)!) - data.append(fileData) - data.append("\r\n".data(using: .utf8)!) + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg + ) + ) } - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - return data + return result } } - public struct AddVoiceInput { - let name: String - let description: String - let fileURL: URL + public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let name: String + public let description: String + public let fileURL: URL - func createMultipartFormData(boundary: String) -> Data { - var data = Data() - - // Add name and description - let parameters = [ - ("name", name), - ("description", description), - ("labels", "") - ] + public init( + name: String, + description: String, + fileURL: URL + ) { + self.name = name + self.description = description + self.fileURL = fileURL + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - for (key, value) in parameters { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - data.append("\(value)\r\n".data(using: .utf8)!) - } + result.append(.text(named: "name", value: name)) + result.append(.text(named: "description", value: description)) + result.append(.text(named: "labels", value: "")) - // Add audio file if let fileData = try? Data(contentsOf: fileURL) { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(fileURL.lastPathComponent)\"\r\n".data(using: .utf8)!) - data.append("Content-Type: audio/x-wav\r\n\r\n".data(using: .utf8)!) - data.append(fileData) - data.append("\r\n".data(using: .utf8)!) + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .wav + ) + ) } - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - return data + return result } } - public struct EditVoiceInput { - let voiceId: String - let name: String - let description: String - let fileURL: URL + public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let name: String + public let description: String + public let fileURL: URL - func createMultipartFormData(boundary: String) -> Data { - var data = Data() - - // Add name and description - let parameters = [ - ("name", name), - ("description", description), - ("labels", "") - ] + public init( + voiceId: String, + name: String, + description: String, + fileURL: URL + ) { + self.voiceId = voiceId + self.name = name + self.description = description + self.fileURL = fileURL + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - for (key, value) in parameters { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - data.append("\(value)\r\n".data(using: .utf8)!) - } + result.append(.text(named: "name", value: name)) + result.append(.text(named: "description", value: description)) + result.append(.text(named: "labels", value: "")) - // Add audio file if let fileData = try? Data(contentsOf: fileURL) { - data.append("--\(boundary)\r\n".data(using: .utf8)!) - data.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(fileURL.lastPathComponent)\"\r\n".data(using: .utf8)!) - data.append("Content-Type: audio/x-wav\r\n\r\n".data(using: .utf8)!) - data.append(fileData) - data.append("\r\n".data(using: .utf8)!) + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .wav + ) + ) } - data.append("--\(boundary)--\r\n".data(using: .utf8)!) - return data + return result } } } +} + +// Client API Methods +extension ElevenLabs.Client { + public func availableVoices() async throws -> [ElevenLabs.Voice] { + try await run(\.listVoices).voices + } - public enum ResponseBodies { - public struct Voices: Codable { - public let voices: [ElevenLabs.Voice] - } + @discardableResult + public func speech( + for text: String, + voiceID: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) async throws -> Data { + let requestBody = ElevenLabs.APISpecification.RequestBodies.SpeechRequest( + text: text, + voiceSettings: voiceSettings, + model: model + ) - public struct VoiceID: Codable { - public let voiceId: String - } + return try await run(\.textToSpeech, with: .init(voiceId: voiceID, requestBody: requestBody)) + } + + public func speechToSpeech( + inputAudioURL: URL, + voiceID: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) async throws -> Data { + let input = ElevenLabs.APISpecification.RequestBodies.SpeechToSpeechInput( + voiceId: voiceID, + audioURL: inputAudioURL, + model: model, + voiceSettings: voiceSettings + ) + + return try await run(\.speechToSpeech, with: input) + } + + public func upload( + voiceWithName name: String, + description: String, + fileURL: URL + ) async throws -> ElevenLabs.Voice.ID { + let input = ElevenLabs.APISpecification.RequestBodies.AddVoiceInput( + name: name, + description: description, + fileURL: fileURL + ) + + let response = try await run(\.addVoice, with: input) + return try .init(rawValue: response.voiceId) + } + + public func edit( + voice: ElevenLabs.Voice.ID, + name: String, + description: String, + fileURL: URL + ) async throws -> Bool { + let input = ElevenLabs.APISpecification.RequestBodies.EditVoiceInput( + voiceId: voice.rawValue, + name: name, + description: description, + fileURL: fileURL + ) + + return try await run(\.editVoice, with: input) + } + + public func delete( + voice: ElevenLabs.Voice.ID + ) async throws { + try await run(\.deleteVoice, with: voice.rawValue) } } diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift index 67b45da1..1c5bd481 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift @@ -5,7 +5,7 @@ import Foundation extension ElevenLabs { - public final class VoiceSettings: Codable, Sendable { + public struct VoiceSettings: Codable, Sendable, Hashable { public enum Setting: String, Codable, Sendable { case stability @@ -54,7 +54,7 @@ extension ElevenLabs { self.removeBackgroundNoise = removeBackgroundNoise ?? false } - public convenience init(stability: Double) { + public init(stability: Double) { self.init( stability: stability, similarityBoost: 0.75, @@ -64,7 +64,7 @@ extension ElevenLabs { ) } - public convenience init(similarityBoost: Double) { + public init(similarityBoost: Double) { self.init( stability: 0.5, similarityBoost: similarityBoost, @@ -74,7 +74,7 @@ extension ElevenLabs { ) } - public convenience init(styleExaggeration: Double) { + public init(styleExaggeration: Double) { self.init( stability: 0.5, similarityBoost: 0.75, @@ -84,7 +84,7 @@ extension ElevenLabs { ) } - public convenience init(speakerBoost: Bool) { + public init(speakerBoost: Bool) { self.init( stability: 0.5, similarityBoost: 0.75, From 8d5d0d31dbef8da2eede9fd02d7a0f8f5d47ec49 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 16:35:30 -0700 Subject: [PATCH 03/73] fixes --- .../ElevenLabs.APISpecification.swift | 2 +- .../Intramodular/ElevenLabs.Client.swift | 86 ++++++++++++++----- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift index 4b55c824..4f9a1031 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift @@ -65,7 +65,7 @@ extension ElevenLabs { @Path({ context -> String in "/v1/text-to-speech/\(context.input.voiceId)" }) - @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) + @Body(json: \.requestBody, keyEncodingStrategy: .convertToSnakeCase) var textToSpeech = Endpoint() // Speech to speech diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index c1a722df..c7da2841 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -41,6 +41,12 @@ extension ElevenLabs.APISpecification { public let voiceSettings: ElevenLabs.VoiceSettings public let model: ElevenLabs.Model + private enum CodingKeys: String, CodingKey { + case text + case voiceSettings = "voice_settings" + case model = "model_id" + } + public init( text: String, voiceSettings: ElevenLabs.VoiceSettings, @@ -50,12 +56,6 @@ extension ElevenLabs.APISpecification { self.voiceSettings = voiceSettings self.model = model } - - public static func == (lhs: SpeechRequest, rhs: SpeechRequest) -> Bool { - return lhs.text == rhs.text && - lhs.voiceSettings == rhs.voiceSettings && - lhs.model == rhs.model - } } public struct TextToSpeechInput: Codable, Hashable { @@ -89,13 +89,26 @@ extension ElevenLabs.APISpecification { public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() - result.append(.text(named: "model_id", value: model.rawValue)) + result.append( + .text( + named: "model_id", + value: model.rawValue + ) + ) let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase if let voiceSettingsData = try? encoder.encode(voiceSettings), - let voiceSettingsString = String(data: voiceSettingsData, encoding: .utf8) { - result.append(.text(named: "voice_settings", value: voiceSettingsString)) + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { + result.append( + .text( + named: "voice_settings", + value: voiceSettingsString + ) + ) } if let fileData = try? Data(contentsOf: audioURL) { @@ -131,9 +144,26 @@ extension ElevenLabs.APISpecification { public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() - result.append(.text(named: "name", value: name)) - result.append(.text(named: "description", value: description)) - result.append(.text(named: "labels", value: "")) + result.append( + .text( + named: "name", + value: name + ) + ) + + result.append( + .text( + named: "description", + value: description + ) + ) + + result.append( + .text( + named: "labels", + value: "" + ) + ) if let fileData = try? Data(contentsOf: fileURL) { result.append( @@ -153,35 +183,43 @@ extension ElevenLabs.APISpecification { public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { public let voiceId: String public let name: String - public let description: String - public let fileURL: URL + public let description: String? + public let fileURL: URL? + public let removeBackgroundNoise: Bool public init( voiceId: String, name: String, - description: String, - fileURL: URL + description: String? = nil, + fileURL: URL? = nil, + removeBackgroundNoise: Bool = false ) { self.voiceId = voiceId self.name = name self.description = description self.fileURL = fileURL + self.removeBackgroundNoise = removeBackgroundNoise } public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() result.append(.text(named: "name", value: name)) - result.append(.text(named: "description", value: description)) - result.append(.text(named: "labels", value: "")) - if let fileData = try? Data(contentsOf: fileURL) { + if let description = description { + result.append(.text(named: "description", value: description)) + } + + result.append(.text(named: "remove_background_noise", value: removeBackgroundNoise ? "true" : "false")) + + if let fileURL = fileURL, + let fileData = try? Data(contentsOf: fileURL) { result.append( .file( named: "files", data: fileData, filename: fileURL.lastPathComponent, - contentType: .wav + contentType: .m4a ) ) } @@ -242,20 +280,22 @@ extension ElevenLabs.Client { ) let response = try await run(\.addVoice, with: input) - return try .init(rawValue: response.voiceId) + return .init(rawValue: response.voiceId) } public func edit( voice: ElevenLabs.Voice.ID, name: String, description: String, - fileURL: URL + fileURL: URL, + removeBackgroundNoise: Bool = false ) async throws -> Bool { let input = ElevenLabs.APISpecification.RequestBodies.EditVoiceInput( voiceId: voice.rawValue, name: name, description: description, - fileURL: fileURL + fileURL: fileURL, + removeBackgroundNoise: removeBackgroundNoise ) return try await run(\.editVoice, with: input) From 7f4bbbb9b4a42ae780602646054fb28bba502844 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 18:06:29 -0700 Subject: [PATCH 04/73] Updated Client --- .../ElevenLabs.APISpecification.swift | 7 ++++ .../Intramodular/ElevenLabs.Client.swift | 34 ++++++++----------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift index 4f9a1031..d100dfcc 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift @@ -128,6 +128,9 @@ extension ElevenLabs.APISpecification { context: DecodeOutputContext ) throws -> Output { do { + if Input.self == RequestBodies.EditVoiceInput.self { + print("TEsts") + } try response.validate() } catch { let apiError: Error @@ -164,6 +167,10 @@ extension ElevenLabs.APISpecification { return response.data as! Output } + if Input.self == RequestBodies.EditVoiceInput.self { + print(response) + } + return try response.decode( Output.self, keyDecodingStrategy: .convertFromSnakeCase diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index c7da2841..ae34fa55 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -158,13 +158,6 @@ extension ElevenLabs.APISpecification { ) ) - result.append( - .text( - named: "labels", - value: "" - ) - ) - if let fileData = try? Data(contentsOf: fileURL) { result.append( .file( @@ -185,33 +178,38 @@ extension ElevenLabs.APISpecification { public let name: String public let description: String? public let fileURL: URL? - public let removeBackgroundNoise: Bool public init( voiceId: String, name: String, description: String? = nil, - fileURL: URL? = nil, - removeBackgroundNoise: Bool = false + fileURL: URL? = nil ) { self.voiceId = voiceId self.name = name self.description = description self.fileURL = fileURL - self.removeBackgroundNoise = removeBackgroundNoise } public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() - result.append(.text(named: "name", value: name)) + result.append( + .text( + named: "name", + value: name + ) + ) if let description = description { - result.append(.text(named: "description", value: description)) + result.append( + .text( + named: "description", + value: description + ) + ) } - result.append(.text(named: "remove_background_noise", value: removeBackgroundNoise ? "true" : "false")) - if let fileURL = fileURL, let fileData = try? Data(contentsOf: fileURL) { result.append( @@ -287,15 +285,13 @@ extension ElevenLabs.Client { voice: ElevenLabs.Voice.ID, name: String, description: String, - fileURL: URL, - removeBackgroundNoise: Bool = false + fileURL: URL? ) async throws -> Bool { let input = ElevenLabs.APISpecification.RequestBodies.EditVoiceInput( voiceId: voice.rawValue, name: name, description: description, - fileURL: fileURL, - removeBackgroundNoise: removeBackgroundNoise + fileURL: fileURL ) return try await run(\.editVoice, with: input) From 3428d0d14d148e7cb797c0d7211aa6b607b5d94b Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 19:26:23 -0700 Subject: [PATCH 05/73] Reorganization --- ...nLabs.APISpecification.RequestBodies.swift | 205 ++++++++++++++++++ ...Labs.APISpecification.ResponseBodies.swift | 20 ++ .../ElevenLabs.APISpecification.swift | 12 - .../Intramodular/ElevenLabs.Client.swift | 196 ----------------- 4 files changed, 225 insertions(+), 208 deletions(-) create mode 100644 Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift create mode 100644 Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift rename Sources/ElevenLabs/Intramodular/{ => API}/ElevenLabs.APISpecification.swift (95%) diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..6acaeaa0 --- /dev/null +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift @@ -0,0 +1,205 @@ +// +// ElevenLabs.RequestTypes.swift +// AI +// +// Created by Jared Davidson on 11/18/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension ElevenLabs.APISpecification { + + enum RequestBodies { + public struct SpeechRequest: Codable, Hashable, Equatable { + public let text: String + public let voiceSettings: ElevenLabs.VoiceSettings + public let model: ElevenLabs.Model + + private enum CodingKeys: String, CodingKey { + case text + case voiceSettings = "voice_settings" + case model = "model_id" + } + + public init( + text: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) { + self.text = text + self.voiceSettings = voiceSettings + self.model = model + } + } + + public struct TextToSpeechInput: Codable, Hashable { + public let voiceId: String + public let requestBody: SpeechRequest + + public init(voiceId: String, requestBody: SpeechRequest) { + self.voiceId = voiceId + self.requestBody = requestBody + } + } + + public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let audioURL: URL + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings + + public init( + voiceId: String, + audioURL: URL, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.model = model + self.voiceSettings = voiceSettings + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .text( + named: "model_id", + value: model.rawValue + ) + ) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { + result.append( + .text( + named: "voice_settings", + value: voiceSettingsString + ) + ) + } + + if let fileData = try? Data(contentsOf: audioURL) { + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg + ) + ) + } + + return result + } + } + + public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let name: String + public let description: String + public let fileURL: URL + + public init( + name: String, + description: String, + fileURL: URL + ) { + self.name = name + self.description = description + self.fileURL = fileURL + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .text( + named: "name", + value: name + ) + ) + + result.append( + .text( + named: "description", + value: description + ) + ) + + if let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .wav + ) + ) + } + + return result + } + } + + public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let name: String + public let description: String? + public let fileURL: URL? + + public init( + voiceId: String, + name: String, + description: String? = nil, + fileURL: URL? = nil + ) { + self.voiceId = voiceId + self.name = name + self.description = description + self.fileURL = fileURL + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .text( + named: "name", + value: name + ) + ) + + if let description = description { + result.append( + .text( + named: "description", + value: description + ) + ) + } + + if let fileURL = fileURL, + let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .m4a + ) + ) + } + + return result + } + } + } +} diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift new file mode 100644 index 00000000..b80abbd5 --- /dev/null +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift @@ -0,0 +1,20 @@ +// +// ElevenLabs.APISpecification.ResponseBodies.swift +// AI +// +// Created by Jared Davidson on 11/18/24. +// + +import Foundation + +extension ElevenLabs.APISpecification { + public enum ResponseBodies { + public struct Voices: Codable { + public let voices: [ElevenLabs.Voice] + } + + public struct VoiceID: Codable { + public let voiceId: String + } + } +} diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift similarity index 95% rename from Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift rename to Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift index d100dfcc..ce9e2742 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift @@ -178,15 +178,3 @@ extension ElevenLabs.APISpecification { } } } - -extension ElevenLabs.APISpecification { - public enum ResponseBodies { - public struct Voices: Codable { - public let voices: [ElevenLabs.Voice] - } - - public struct VoiceID: Codable { - public let voiceId: String - } - } -} diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index ae34fa55..2093a3ea 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -33,202 +33,6 @@ extension ElevenLabs { } } -extension ElevenLabs.APISpecification { - - enum RequestBodies { - public struct SpeechRequest: Codable, Hashable, Equatable { - public let text: String - public let voiceSettings: ElevenLabs.VoiceSettings - public let model: ElevenLabs.Model - - private enum CodingKeys: String, CodingKey { - case text - case voiceSettings = "voice_settings" - case model = "model_id" - } - - public init( - text: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) { - self.text = text - self.voiceSettings = voiceSettings - self.model = model - } - } - - public struct TextToSpeechInput: Codable, Hashable { - public let voiceId: String - public let requestBody: SpeechRequest - - public init(voiceId: String, requestBody: SpeechRequest) { - self.voiceId = voiceId - self.requestBody = requestBody - } - } - - public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let audioURL: URL - public let model: ElevenLabs.Model - public let voiceSettings: ElevenLabs.VoiceSettings - - public init( - voiceId: String, - audioURL: URL, - model: ElevenLabs.Model, - voiceSettings: ElevenLabs.VoiceSettings - ) { - self.voiceId = voiceId - self.audioURL = audioURL - self.model = model - self.voiceSettings = voiceSettings - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "model_id", - value: model.rawValue - ) - ) - - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - if let voiceSettingsData = try? encoder.encode(voiceSettings), - let voiceSettingsString = String( - data: voiceSettingsData, - encoding: .utf8 - ) { - result.append( - .text( - named: "voice_settings", - value: voiceSettingsString - ) - ) - } - - if let fileData = try? Data(contentsOf: audioURL) { - result.append( - .file( - named: "audio", - data: fileData, - filename: audioURL.lastPathComponent, - contentType: .mpeg - ) - ) - } - - return result - } - } - - public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let name: String - public let description: String - public let fileURL: URL - - public init( - name: String, - description: String, - fileURL: URL - ) { - self.name = name - self.description = description - self.fileURL = fileURL - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "name", - value: name - ) - ) - - result.append( - .text( - named: "description", - value: description - ) - ) - - if let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .wav - ) - ) - } - - return result - } - } - - public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let name: String - public let description: String? - public let fileURL: URL? - - public init( - voiceId: String, - name: String, - description: String? = nil, - fileURL: URL? = nil - ) { - self.voiceId = voiceId - self.name = name - self.description = description - self.fileURL = fileURL - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "name", - value: name - ) - ) - - if let description = description { - result.append( - .text( - named: "description", - value: description - ) - ) - } - - if let fileURL = fileURL, - let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .m4a - ) - ) - } - - return result - } - } - } -} - -// Client API Methods extension ElevenLabs.Client { public func availableVoices() async throws -> [ElevenLabs.Voice] { try await run(\.listVoices).voices From 4e412285c307c6ffc149b89a252b7a40dec0141b Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 18 Nov 2024 19:32:01 -0700 Subject: [PATCH 06/73] m4a --- .../API/ElevenLabs.APISpecification.RequestBodies.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift index 6acaeaa0..3404e48e 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift @@ -140,7 +140,7 @@ extension ElevenLabs.APISpecification { named: "files", data: fileData, filename: fileURL.lastPathComponent, - contentType: .wav + contentType: .m4a ) ) } From 335bcd35f7d14c0e8c2ff6751b4a7006aac68605 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 09:35:49 -0700 Subject: [PATCH 07/73] Inital --- .../Intramodular/API/PlayHT.Client.swift | 22 +++++++++++++++++++ .../Intramodular/API/PlayHT.Model.swift | 7 ++++++ Sources/PlayHT/Intramodular/API/PlayHT.swift | 12 ++++++++++ Sources/PlayHT/module.swift | 6 +++++ 4 files changed, 47 insertions(+) create mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.Client.swift create mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.Model.swift create mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.swift create mode 100644 Sources/PlayHT/module.swift diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift new file mode 100644 index 00000000..a6571657 --- /dev/null +++ b/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift @@ -0,0 +1,22 @@ +// +// PlayHT.Client.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import CorePersistence +import Diagnostics +import NetworkKit +import Foundation +import SwiftAPI +import Merge +import FoundationX +import Swallow + +extension PlayHT { + @RuntimeDiscoverable + public final class Client: SwiftAPI.Client, ObservableObject { + + } +} diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/API/PlayHT.Model.swift new file mode 100644 index 00000000..3f61f1e0 --- /dev/null +++ b/Sources/PlayHT/Intramodular/API/PlayHT.Model.swift @@ -0,0 +1,7 @@ +// +// PlayHT.Model.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.swift b/Sources/PlayHT/Intramodular/API/PlayHT.swift new file mode 100644 index 00000000..19f94203 --- /dev/null +++ b/Sources/PlayHT/Intramodular/API/PlayHT.swift @@ -0,0 +1,12 @@ +// +// PlayHT.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import Swift + +public enum PlayHT { + +} diff --git a/Sources/PlayHT/module.swift b/Sources/PlayHT/module.swift new file mode 100644 index 00000000..1fa9fa48 --- /dev/null +++ b/Sources/PlayHT/module.swift @@ -0,0 +1,6 @@ +// +// Copyright (c) Vatsal Manot +// + +@_exported import Swallow +@_exported import SwallowMacrosClient From 02bb57a63eda524ac3e0bc89b9d1e847da902e0c Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 09:54:02 -0700 Subject: [PATCH 08/73] Initial runthrough --- ...layHT.APISpecification.RequestBodies.swift | 90 ++++++++++++++++ ...el.swift => PlayHT.APISpecification.swift} | 2 +- ...yHT.APISpeicification.ResponseBodies.swift | 27 +++++ .../Intramodular/API/PlayHT.Client.swift | 22 ---- .../PlayHT/Intramodular/PlayHT.Client.swift | 102 ++++++++++++++++++ .../PlayHT/Intramodular/PlayHT.Model.swift | 35 ++++++ .../Intramodular/{API => }/PlayHT.swift | 0 7 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift rename Sources/PlayHT/Intramodular/API/{PlayHT.Model.swift => PlayHT.APISpecification.swift} (63%) create mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift delete mode 100644 Sources/PlayHT/Intramodular/API/PlayHT.Client.swift create mode 100644 Sources/PlayHT/Intramodular/PlayHT.Client.swift create mode 100644 Sources/PlayHT/Intramodular/PlayHT.Model.swift rename Sources/PlayHT/Intramodular/{API => }/PlayHT.swift (100%) diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..f9c1f2ec --- /dev/null +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -0,0 +1,90 @@ +// +// PlayHT.APISpecification.RequestBodies.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension PlayHT.APISpecification { + enum RequestBodies { + public struct TextToSpeechInput: Codable, Hashable { + public let text: String + public let voiceId: String + public let quality: String + public let outputFormat: String + public let speed: Double? + public let sampleRate: Int? + + public init( + text: String, + voiceId: String, + quality: String = "medium", + outputFormat: String = "mp3", + speed: Double? = nil, + sampleRate: Int? = nil + ) { + self.text = text + self.voiceId = voiceId + self.quality = quality + self.outputFormat = outputFormat + self.speed = speed + self.sampleRate = sampleRate + } + } + + public struct CloneVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let name: String + public let description: String? + public let fileURLs: [URL] + + public init( + name: String, + description: String? = nil, + fileURLs: [URL] + ) { + self.name = name + self.description = description + self.fileURLs = fileURLs + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .text( + named: "name", + value: name + ) + ) + + if let description = description { + result.append( + .text( + named: "description", + value: description + ) + ) + } + + for (index, fileURL) in fileURLs.enumerated() { + if let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files[\(index)]", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .wav + ) + ) + } + } + + return result + } + } + } +} diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift similarity index 63% rename from Sources/PlayHT/Intramodular/API/PlayHT.Model.swift rename to Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index 3f61f1e0..f48b54d0 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.Model.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -1,5 +1,5 @@ // -// PlayHT.Model.swift +// PlayHT.APISpecification.swift // AI // // Created by Jared Davidson on 11/20/24. diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift new file mode 100644 index 00000000..d462768c --- /dev/null +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift @@ -0,0 +1,27 @@ +// +// PlayHT.APISpeicification.ResponseBodies.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import Foundation + +extension PlayHT.APISpecification { + public enum ResponseBodies { + public struct Voices: Codable { + public let voices: [PlayHT.Voice] + } + + public struct TextToSpeechOutput: Codable { + public let transcriptionId: String + public let audioUrl: String? + } + + public struct ClonedVoiceOutput: Codable { + public let id: String + public let name: String + public let status: String + } + } +} diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift deleted file mode 100644 index a6571657..00000000 --- a/Sources/PlayHT/Intramodular/API/PlayHT.Client.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// PlayHT.Client.swift -// AI -// -// Created by Jared Davidson on 11/20/24. -// - -import CorePersistence -import Diagnostics -import NetworkKit -import Foundation -import SwiftAPI -import Merge -import FoundationX -import Swallow - -extension PlayHT { - @RuntimeDiscoverable - public final class Client: SwiftAPI.Client, ObservableObject { - - } -} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift new file mode 100644 index 00000000..fb9f6380 --- /dev/null +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -0,0 +1,102 @@ +// +// PlayHT.Client.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension PlayHT { + @RuntimeDiscoverable + public final class Client: SwiftAPI.Client, ObservableObject { + public typealias API = PlayHT.APISpecification + public typealias Session = HTTPSession + + public let interface: API + public let session: Session + public var sessionCache: EmptyKeyedCache + + public required init(configuration: API.Configuration) { + self.interface = API(configuration: configuration) + self.session = HTTPSession.shared + self.sessionCache = .init() + } + + public convenience init(apiKey: String?, userId: String?) { + self.init(configuration: .init(apiKey: apiKey, userId: userId)) + } + } +} + +extension PlayHT.Client { + public func availableVoices() async throws -> [PlayHT.Voice] { + try await run(\.listVoices).voices + } + + public func generateSpeech( + text: String, + voiceId: String, + quality: String = "medium", + outputFormat: String = "mp3", + speed: Double? = nil, + sampleRate: Int? = nil + ) async throws -> String? { + let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( + text: text, + voiceId: voiceId, + quality: quality, + outputFormat: outputFormat, + speed: speed, + sampleRate: sampleRate + ) + + let response = try await run(\.textToSpeech, with: input) + return response.audioUrl + } + + public func streamSpeech( + text: String, + voiceId: String, + quality: String = "medium", + outputFormat: String = "mp3", + speed: Double? = nil, + sampleRate: Int? = nil + ) async throws -> Data { + let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( + text: text, + voiceId: voiceId, + quality: quality, + outputFormat: outputFormat, + speed: speed, + sampleRate: sampleRate + ) + + return try await run(\.streamTextToSpeech, with: input) + } + + public func cloneVoice( + name: String, + description: String? = nil, + fileURLs: [URL] + ) async throws -> (id: String, name: String, status: String) { + let input = PlayHT.APISpecification.RequestBodies.CloneVoiceInput( + name: name, + description: description, + fileURLs: fileURLs + ) + + let response = try await run(\.cloneVoice, with: input) + return (response.id, response.name, response.status) + } + + public func deleteClonedVoice( + id: String + ) async throws { + try await run(\.deleteClonedVoice, with: id) + } +} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/PlayHT.Model.swift new file mode 100644 index 00000000..83e615c8 --- /dev/null +++ b/Sources/PlayHT/Intramodular/PlayHT.Model.swift @@ -0,0 +1,35 @@ +// +// PlayHT.Model.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import CoreMI +import CorePersistence +import Foundation +import Swift + +extension PlayHT { + public struct Voice: Codable, Hashable, Identifiable { + public let id: String + public let name: String + public let category: String? + public let language: String + public let gender: String? + public let isCloned: Bool + public let isPreview: Bool + public let previewUrl: String? + + enum CodingKeys: String, CodingKey { + case id + case name + case category + case language + case gender + case isCloned = "cloned" + case isPreview = "preview" + case previewUrl + } + } +} diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.swift b/Sources/PlayHT/Intramodular/PlayHT.swift similarity index 100% rename from Sources/PlayHT/Intramodular/API/PlayHT.swift rename to Sources/PlayHT/Intramodular/PlayHT.swift From 0c8bc0219410aa2950373ce158298bc660697429 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 10:09:17 -0700 Subject: [PATCH 09/73] MIService --- .../ModelIdentifier.Provider.swift | 187 +++++++++--------- .../Service/_MIServiceTypeIdentifier.swift | 1 + .../PlayHT/Intramodular/PlayHT.Client.swift | 19 ++ 3 files changed, 119 insertions(+), 88 deletions(-) diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index fbdc91cc..6917b038 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -24,6 +24,7 @@ extension ModelIdentifier { case _Cohere case _ElevenLabs case _TogetherAI + case _PlayHT case unknown(String) @@ -74,6 +75,10 @@ extension ModelIdentifier { public static var togetherAI: Self { Self._TogetherAI } + + public static var playHT: Self { + Self._PlayHT + } } } @@ -82,36 +87,38 @@ extension ModelIdentifier { extension ModelIdentifier.Provider: CustomStringConvertible { public var description: String { switch self { - case ._Anthropic: - return "Anthropic" - case ._Apple: - return "Apple" - case ._Fal: - return "Fal" - case ._Mistral: - return "Mistral" - case ._Groq: - return "Groq" - case ._Ollama: - return "Ollama" - case ._OpenAI: - return "OpenAI" - case ._Gemini: - return "Gemini" - case ._Perplexity: - return "Perplexity" - case ._Jina: - return "Perplexity" - case ._VoyageAI: - return "VoyageAI" - case ._Cohere: - return "Cohere" - case ._ElevenLabs: - return "ElevenLabs" - case ._TogetherAI: - return "TogetherAI" - case .unknown(let provider): - return provider + case ._Anthropic: + return "Anthropic" + case ._Apple: + return "Apple" + case ._Fal: + return "Fal" + case ._Mistral: + return "Mistral" + case ._Groq: + return "Groq" + case ._Ollama: + return "Ollama" + case ._OpenAI: + return "OpenAI" + case ._Gemini: + return "Gemini" + case ._Perplexity: + return "Perplexity" + case ._Jina: + return "Perplexity" + case ._VoyageAI: + return "VoyageAI" + case ._Cohere: + return "Cohere" + case ._ElevenLabs: + return "ElevenLabs" + case ._TogetherAI: + return "TogetherAI" + case ._PlayHT: + return "PlayHT" + case .unknown(let provider): + return provider } } } @@ -119,69 +126,73 @@ extension ModelIdentifier.Provider: CustomStringConvertible { extension ModelIdentifier.Provider: RawRepresentable { public var rawValue: String { switch self { - case ._Anthropic: - return "anthropic" - case ._Apple: - return "apple" - case ._Fal: - return "fal" - case ._Mistral: - return "mistral" - case ._Groq: - return "groq" - case ._Ollama: - return "ollama" - case ._OpenAI: - return "openai" - case ._Gemini: - return "gemini" - case ._Perplexity: - return "perplexity" - case ._Jina: - return "jina" - case ._VoyageAI: - return "voyageai" - case ._Cohere: - return "cohere" - case ._ElevenLabs: - return "elevenlabs" - case ._TogetherAI: - return "togetherai" - case .unknown(let provider): - return provider + case ._Anthropic: + return "anthropic" + case ._Apple: + return "apple" + case ._Fal: + return "fal" + case ._Mistral: + return "mistral" + case ._Groq: + return "groq" + case ._Ollama: + return "ollama" + case ._OpenAI: + return "openai" + case ._Gemini: + return "gemini" + case ._Perplexity: + return "perplexity" + case ._Jina: + return "jina" + case ._VoyageAI: + return "voyageai" + case ._Cohere: + return "cohere" + case ._ElevenLabs: + return "elevenlabs" + case ._TogetherAI: + return "togetherai" + case ._PlayHT: + return "playht" + case .unknown(let provider): + return provider } } public init(rawValue: String) { switch rawValue { - case Self._Anthropic.rawValue: - self = ._Anthropic - case Self._Apple.rawValue: - self = ._Apple - case Self._Fal.rawValue: - self = ._Fal - case Self._Mistral.rawValue: - self = ._Mistral - case Self._Groq.rawValue: - self = ._Groq - case Self._OpenAI.rawValue: - self = ._OpenAI - case Self._Gemini.rawValue: - self = ._Gemini - case Self._Perplexity.rawValue: - self = ._Perplexity - case Self._Jina.rawValue: - self = ._Jina - case Self._VoyageAI.rawValue: - self = ._VoyageAI - case Self._Cohere.rawValue: - self = ._Cohere - case Self._ElevenLabs.rawValue: - self = ._ElevenLabs - case Self._TogetherAI.rawValue: - self = ._TogetherAI - default: - self = .unknown(rawValue) + case Self._Anthropic.rawValue: + self = ._Anthropic + case Self._Apple.rawValue: + self = ._Apple + case Self._Fal.rawValue: + self = ._Fal + case Self._Mistral.rawValue: + self = ._Mistral + case Self._Groq.rawValue: + self = ._Groq + case Self._OpenAI.rawValue: + self = ._OpenAI + case Self._Gemini.rawValue: + self = ._Gemini + case Self._Perplexity.rawValue: + self = ._Perplexity + case Self._Jina.rawValue: + self = ._Jina + case Self._VoyageAI.rawValue: + self = ._VoyageAI + case Self._Cohere.rawValue: + self = ._Cohere + case Self._ElevenLabs.rawValue: + self = ._ElevenLabs + case Self._TogetherAI.rawValue: + self = ._TogetherAI + case Self._PlayHT.rawValue: + self = ._PlayHT + default: + self = .unknown(rawValue) } } } diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift index 00fe0f99..e7ffab25 100644 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift +++ b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift @@ -38,4 +38,5 @@ extension _MIServiceTypeIdentifier { public static let _VoyageAI = _MIServiceTypeIdentifier(rawValue: "hajat-fufoh-janaf-disam") public static let _Cohere = _MIServiceTypeIdentifier(rawValue: "guzob-fipin-navij-duvon") public static let _TogetherAI = _MIServiceTypeIdentifier(rawValue: "pafob-vopoj-lurig-zilur") + public static let _PlayHT = _MIServiceTypeIdentifier(rawValue: "foluv-jufuk-zuhok-hofid") } diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index fb9f6380..73e2fe37 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -33,6 +33,25 @@ extension PlayHT { } } +extension PlayHT.Client: _MIService { + public convenience init( + account: (any _MIServiceAccount)? + ) async throws { + let account: any _MIServiceAccount = try account.unwrap() + let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + + guard serviceIdentifier == _MIServiceTypeIdentifier._PlayHT else { + throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + } + + guard let credential = account.credential as? _MIServiceAPIKeyCredential else { + throw _MIServiceError.invalidCredentials(account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} + extension PlayHT.Client { public func availableVoices() async throws -> [PlayHT.Voice] { try await run(\.listVoices).voices From 665b898a909a8e6c6227088bc72a90be49c0f5c9 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 11:25:04 -0700 Subject: [PATCH 10/73] Updates & Tests --- Package.swift | 27 ++++ ...layHT.APISpecification.RequestBodies.swift | 92 ++++++----- .../API/PlayHT.APISpecification.swift | 148 ++++++++++++++++++ .../Models/PlayHT.OutputSettings.swift | 28 ++++ .../Intramodular/Models/PlayHT.Voice.swift | 74 +++++++++ .../Models/PlayHT.VoiceSettings.swift | 30 ++++ .../PlayHT/Intramodular/PlayHT.Client.swift | 90 ++++++----- .../PlayHT/Intramodular/PlayHT.Model.swift | 57 ++++--- Tests/PlayHT/Intramodular/SpeechTests.swift | 74 +++++++++ Tests/PlayHT/module.swift | 15 ++ 10 files changed, 544 insertions(+), 91 deletions(-) create mode 100644 Sources/PlayHT/Intramodular/Models/PlayHT.OutputSettings.swift create mode 100644 Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift create mode 100644 Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift create mode 100644 Tests/PlayHT/Intramodular/SpeechTests.swift create mode 100644 Tests/PlayHT/module.swift diff --git a/Package.swift b/Package.swift index e4abb826..6c3f83ab 100644 --- a/Package.swift +++ b/Package.swift @@ -105,6 +105,21 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .target( + name: "PlayHT", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/PlayHT", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .target( name: "_Gemini", dependencies: [ @@ -278,6 +293,7 @@ let package = Package( "Ollama", "OpenAI", "Perplexity", + "PlayHT", "Swallow", "Jina", "VoyageAI", @@ -367,6 +383,17 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .testTarget( + name: "PlayHTTests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/PlayHT", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .testTarget( name: "JinaTests", dependencies: [ diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index f9c1f2ec..c568651d 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -13,42 +13,73 @@ extension PlayHT.APISpecification { enum RequestBodies { public struct TextToSpeechInput: Codable, Hashable { public let text: String - public let voiceId: String + public let voice: String + public let voiceEngine: PlayHT.Model public let quality: String public let outputFormat: String public let speed: Double? public let sampleRate: Int? + public let seed: Int? + public let temperature: Double? + public let emotion: String? + public let voiceGuidance: Double? + public let styleGuidance: Double? + public let textGuidance: Double? + public let language: String? + + private enum CodingKeys: String, CodingKey { + case text, voice, quality + case voiceEngine = "voice_engine" + case outputFormat = "output_format" + case speed + case sampleRate = "sample_rate" + case seed, temperature, emotion + case voiceGuidance = "voice_guidance" + case styleGuidance = "style_guidance" + case textGuidance = "text_guidance" + case language + } public init( text: String, - voiceId: String, + voice: String, + voiceEngine: PlayHT.Model = .play3Mini, quality: String = "medium", outputFormat: String = "mp3", speed: Double? = nil, - sampleRate: Int? = nil + sampleRate: Int? = 48000, + seed: Int? = nil, + temperature: Double? = nil, + emotion: String? = nil, + voiceGuidance: Double? = nil, + styleGuidance: Double? = nil, + textGuidance: Double? = nil, + language: String? = nil ) { self.text = text - self.voiceId = voiceId + self.voice = voice + self.voiceEngine = voiceEngine self.quality = quality self.outputFormat = outputFormat self.speed = speed self.sampleRate = sampleRate + self.seed = seed + self.temperature = temperature + self.emotion = emotion + self.voiceGuidance = voiceGuidance + self.styleGuidance = styleGuidance + self.textGuidance = textGuidance + self.language = language } } - public struct CloneVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { - public let name: String - public let description: String? - public let fileURLs: [URL] + public struct InstantCloneVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let sampleFileURL: String + public let voiceName: String - public init( - name: String, - description: String? = nil, - fileURLs: [URL] - ) { - self.name = name - self.description = description - self.fileURLs = fileURLs + public init(sampleFileURL: String, voiceName: String) { + self.sampleFileURL = sampleFileURL + self.voiceName = voiceName } public func __conversion() throws -> HTTPRequest.Multipart.Content { @@ -56,32 +87,17 @@ extension PlayHT.APISpecification { result.append( .text( - named: "name", - value: name + named: "sample_file_url", + value: sampleFileURL ) ) - if let description = description { - result.append( - .text( - named: "description", - value: description - ) + result.append( + .text( + named: "voice_name", + value: voiceName ) - } - - for (index, fileURL) in fileURLs.enumerated() { - if let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files[\(index)]", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .wav - ) - ) - } - } + ) return result } diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index f48b54d0..dddaa30d 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -5,3 +5,151 @@ // Created by Jared Davidson on 11/20/24. // +import CorePersistence +import Diagnostics +import NetworkKit +import Swift +import SwiftAPI + +extension PlayHT { + public enum APIError: APIErrorProtocol { + public typealias API = PlayHT.APISpecification + + case apiKeyMissing + case userIdMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case invalidContentType + case badRequest(request: API.Request?, error: API.Request.Error) + case unknown(message: String) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var host: URL + public var apiKey: String? + public var userId: String? + + public init( + host: URL = URL(string: "https://api.play.ht/api/v2")!, + apiKey: String? = nil, + userId: String? = nil + ) { + self.host = host + self.apiKey = apiKey + self.userId = userId + } + } + + public let configuration: Configuration + + public var host: URL { + configuration.host + } + + public var id: some Hashable { + configuration + } + + public init(configuration: Configuration) { + self.configuration = configuration + } + + // Voices endpoints + @GET + @Path("/voices") + var listVoices = Endpoint() + + // Text to speech + @POST + @Path("/tts") + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) + var textToSpeech = Endpoint() + + // Stream text to speech + @POST + @Path("/tts/stream") + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) + var streamTextToSpeech = Endpoint() + + // Clone Voice + @POST + @Path("/cloned-voices") + var listClonedVoices = Endpoint() + + @POST + @Path("/cloned-voices/instant") + @Body(multipart: .input) + var instantCloneVoice = Endpoint() + + // Delete cloned voice + @DELETE + @Path({ context -> String in + "/cloned-voices/\(context.input)" + }) + var deleteClonedVoice = Endpoint() + } +} + +extension PlayHT.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + var request = try super.buildRequestBase( + from: input, + context: context + ) + + request = request + .header("x-api-key", context.root.configuration.apiKey) + .header("accept", "application/json") + .header("AUTHORIZATION", context.root.configuration.userId) + .header(.contentType(.json)) + + return request + } + + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? HTTPRequest.Error { + if response.statusCode.rawValue == 401 { + apiError = .incorrectAPIKeyProvided + } else if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + if Output.self == Data.self { + return response.data as! Output + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.OutputSettings.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.OutputSettings.swift new file mode 100644 index 00000000..88b520c6 --- /dev/null +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.OutputSettings.swift @@ -0,0 +1,28 @@ +// +// PlayHT.OutputSettings.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import Swift + +extension PlayHT { + public struct OutputSettings: Codable, Hashable { + public let quality: Quality + public let format: OutputFormat + public let sampleRate: Int + + public init( + quality: Quality = .medium, + format: OutputFormat = .mp3, + sampleRate: Int = 48000 + ) { + self.quality = quality + self.format = format + self.sampleRate = sampleRate + } + + public static let `default` = OutputSettings() + } +} diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift new file mode 100644 index 00000000..6fc6c91f --- /dev/null +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift @@ -0,0 +1,74 @@ +// +// PlayHT.Voice.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import Foundation +import ElevenLabs + +extension PlayHT { + public struct Voice: Codable, Hashable, Identifiable { + public typealias ID = _TypeAssociatedID + + public let id: ID + public let name: String + public let language: String + public let voiceEngine: Model + public let isCloned: Bool + public let gender: String? + public let accent: String? + public let age: String? + public let style: String? + public let useCase: String? + + private enum CodingKeys: String, CodingKey { + case id, name, language, gender, accent, age, style + case voiceEngine = "voice_engine" + case isCloned = "cloned" + case useCase = "use_case" + } + } + + public enum Quality: String, Codable { + case draft = "draft" + case low = "low" + case medium = "medium" + case high = "high" + case premium = "premium" + } + + public enum OutputFormat: String, Codable { + case mp3 = "mp3" + case wav = "wav" + case ogg = "ogg" + case mulaw = "mulaw" + case flac = "flac" + } +} + +#warning("This is only a temporary fix. Remove these & replace with Abstract (@jared)") + +extension PlayHT.Voice { + public func toElevenLabsVoice() -> ElevenLabs.Voice { + ElevenLabs.Voice( + voiceID: id.rawValue, + name: name, + description: style, + isOwner: isCloned + ) + } +} + +extension PlayHT.VoiceSettings { + public static func fromElevenLabs(_ settings: ElevenLabs.VoiceSettings) -> Self { + PlayHT.VoiceSettings( + speed: 1.0, + temperature: settings.stability, + voiceGuidance: settings.similarityBoost * 6.0, + styleGuidance: settings.styleExaggeration * 30.0, + textGuidance: 1.0 + settings.similarityBoost + ) + } +} diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift new file mode 100644 index 00000000..ea90cdeb --- /dev/null +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift @@ -0,0 +1,30 @@ +// +// PlayHT.VoiceSettings.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +extension PlayHT { + public struct VoiceSettings: Codable, Hashable { + public var speed: Double + public var temperature: Double + public var voiceGuidance: Double + public var styleGuidance: Double + public var textGuidance: Double + + public init( + speed: Double = 1.0, + temperature: Double = 1.0, + voiceGuidance: Double = 3.0, + styleGuidance: Double = 15.0, + textGuidance: Double = 1.5 + ) { + self.speed = max(0.1, min(5.0, speed)) + self.temperature = max(0, min(2.0, temperature)) + self.voiceGuidance = max(1, min(6.0, voiceGuidance)) + self.styleGuidance = max(1, min(30.0, styleGuidance)) + self.textGuidance = max(1, min(2.0, textGuidance)) + } + } +} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 73e2fe37..1ee76dc5 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -14,6 +14,10 @@ import Swallow extension PlayHT { @RuntimeDiscoverable public final class Client: SwiftAPI.Client, ObservableObject { + public static var persistentTypeRepresentation: some IdentityRepresentation { + _MIServiceTypeIdentifier._PlayHT + } + public typealias API = PlayHT.APISpecification public typealias Session = HTTPSession @@ -27,8 +31,8 @@ extension PlayHT { self.sessionCache = .init() } - public convenience init(apiKey: String?, userId: String?) { - self.init(configuration: .init(apiKey: apiKey, userId: userId)) + public convenience init(apiKey: String) { + self.init(configuration: .init(apiKey: apiKey)) } } } @@ -57,21 +61,29 @@ extension PlayHT.Client { try await run(\.listVoices).voices } + public func clonedVoices() async throws -> [PlayHT.Voice] { + try await run(\.listClonedVoices).voices + } + public func generateSpeech( text: String, - voiceId: String, - quality: String = "medium", - outputFormat: String = "mp3", - speed: Double? = nil, - sampleRate: Int? = nil + voice: PlayHT.Voice, + settings: PlayHT.VoiceSettings, + outputSettings: PlayHT.OutputSettings = .default ) async throws -> String? { let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, - voiceId: voiceId, - quality: quality, - outputFormat: outputFormat, - speed: speed, - sampleRate: sampleRate + voice: voice.id.rawValue, + voiceEngine: voice.voiceEngine, + quality: outputSettings.quality.rawValue, + outputFormat: outputSettings.format.rawValue, + speed: settings.speed, + sampleRate: outputSettings.sampleRate, + temperature: settings.temperature, + voiceGuidance: settings.voiceGuidance, + styleGuidance: settings.styleGuidance, + textGuidance: settings.textGuidance, + language: voice.language ) let response = try await run(\.textToSpeech, with: input) @@ -80,42 +92,50 @@ extension PlayHT.Client { public func streamSpeech( text: String, - voiceId: String, - quality: String = "medium", - outputFormat: String = "mp3", - speed: Double? = nil, - sampleRate: Int? = nil + voice: PlayHT.Voice, + settings: PlayHT.VoiceSettings, + outputSettings: PlayHT.OutputSettings = .default ) async throws -> Data { let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, - voiceId: voiceId, - quality: quality, - outputFormat: outputFormat, - speed: speed, - sampleRate: sampleRate + voice: voice.id.rawValue, + voiceEngine: voice.voiceEngine, + quality: outputSettings.quality.rawValue, + outputFormat: outputSettings.format.rawValue, + speed: settings.speed, + sampleRate: outputSettings.sampleRate, + temperature: settings.temperature, + voiceGuidance: settings.voiceGuidance, + styleGuidance: settings.styleGuidance, + textGuidance: settings.textGuidance, + language: voice.language ) return try await run(\.streamTextToSpeech, with: input) } - public func cloneVoice( - name: String, - description: String? = nil, - fileURLs: [URL] - ) async throws -> (id: String, name: String, status: String) { - let input = PlayHT.APISpecification.RequestBodies.CloneVoiceInput( - name: name, - description: description, - fileURLs: fileURLs + public func instantCloneVoice( + sampleFileURL: String, + name: String + ) async throws -> PlayHT.Voice.ID { + let input = PlayHT.APISpecification.RequestBodies.InstantCloneVoiceInput( + sampleFileURL: sampleFileURL, + voiceName: name ) - let response = try await run(\.cloneVoice, with: input) - return (response.id, response.name, response.status) + let response = try await run(\.instantCloneVoice, with: input) + return .init(rawValue: response.id) } public func deleteClonedVoice( - id: String + voice: PlayHT.Voice.ID ) async throws { - try await run(\.deleteClonedVoice, with: id) + try await run(\.deleteClonedVoice, with: voice.rawValue) + } + + private func findVoice(id: String) async throws -> PlayHT.Voice? { + let allVoices = try await availableVoices() + let clonedVoices = try await clonedVoices() + return (allVoices + clonedVoices).first { $0.id.rawValue == id } } } diff --git a/Sources/PlayHT/Intramodular/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/PlayHT.Model.swift index 83e615c8..560f7643 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Model.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Model.swift @@ -11,25 +11,46 @@ import Foundation import Swift extension PlayHT { - public struct Voice: Codable, Hashable, Identifiable { - public let id: String - public let name: String - public let category: String? - public let language: String - public let gender: String? - public let isCloned: Bool - public let isPreview: Bool - public let previewUrl: String? + public enum Model: String, Codable, Sendable { + /// Latest speech model optimized for realtime use. Features include: + /// - Multilingual support (36 languages) + /// - Reduced hallucinations + /// - <200ms streaming latency + /// - 48kHz sampling + /// - 20k character limit per stream + case play3Mini = "Play3.0-mini" - enum CodingKeys: String, CodingKey { - case id - case name - case category - case language - case gender - case isCloned = "cloned" - case isPreview = "preview" - case previewUrl + /// Legacy voice model with basic TTS capabilities + case playHT2Turbo = "PlayHT2.0-turbo" + } +} + +// MARK: - Conformances + +extension PlayHT.Model: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension PlayHT.Model: ModelIdentifierRepresentable { + public init(from identifier: ModelIdentifier) throws { + guard identifier.provider == ._PlayHT, identifier.revision == nil else { + throw Never.Reason.illegal + } + + guard let model = Self(rawValue: identifier.name) else { + throw Never.Reason.unexpected } + + self = model + } + + public func __conversion() -> ModelIdentifier { + ModelIdentifier( + provider: ._PlayHT, + name: rawValue, + revision: nil + ) } } diff --git a/Tests/PlayHT/Intramodular/SpeechTests.swift b/Tests/PlayHT/Intramodular/SpeechTests.swift new file mode 100644 index 00000000..e4b58291 --- /dev/null +++ b/Tests/PlayHT/Intramodular/SpeechTests.swift @@ -0,0 +1,74 @@ +// +// SpeechTests.swift +// AI +// +// Created by Jared Davidson on 11/20/24. +// + +import PlayHT +import XCTest + +final class PlayHTTests: XCTestCase { + + let sampleText = "In a quiet, unassuming village nestled deep in a lush, verdant valley, young Elara leads a simple life, dreaming of adventure beyond the horizon. Her village is filled with ancient folklore and tales of mystical relics, but none capture her imagination like the legend of the Enchanted Amulet—a powerful artifact said to grant its bearer the ability to control time." + + func testListVoices() async throws { + let voices = try await client.availableVoices() + XCTAssertFalse(voices.isEmpty) + } + + func testCreateSpeech() async throws { + let voices = try await client.availableVoices() + let voice = try XCTUnwrap(voices.first) + + let voiceSettings = PlayHT.VoiceSettings( + speed: 1.0, + temperature: 0.7, + voiceGuidance: 3.0, + styleGuidance: 15.0, + textGuidance: 1.5 + ) + + let outputSettings = PlayHT.OutputSettings( + quality: .high, + format: .mp3, + sampleRate: 48000 + ) + + let audioURL = try await client.generateSpeech( + text: sampleText, + voice: voice, + settings: voiceSettings, + outputSettings: outputSettings + ) + + XCTAssertNotNil(audioURL) + } + + func testStreamSpeech() async throws { + let voices = try await client.availableVoices() + let voice = try XCTUnwrap(voices.first) + + let voiceSettings = PlayHT.VoiceSettings() + let outputSettings = PlayHT.OutputSettings() + + let data = try await client.streamSpeech( + text: sampleText, + voice: voice, + settings: voiceSettings, + outputSettings: outputSettings + ) + + XCTAssertFalse(data.isEmpty) + } + + func testInstantCloneVoice() async throws { + let sampleURL = "https://example.com/sample.mp3" + let voiceID = try await client.instantCloneVoice( + sampleFileURL: sampleURL, + name: "Test Clone" + ) + + XCTAssertNotNil(voiceID) + } +} diff --git a/Tests/PlayHT/module.swift b/Tests/PlayHT/module.swift new file mode 100644 index 00000000..c217577d --- /dev/null +++ b/Tests/PlayHT/module.swift @@ -0,0 +1,15 @@ +// +// Copyright (c) Vatsal Manot +// + +import ElevenLabs + +public var PLAYHT_API_KEY: String { + "0dea648f8b5c9497b647902ae00e6903" +} + +public var client: PlayHT.Client { + let client = PlayHT.Client(apiKey: PLAYHT_API_KEY) + + return client +} From 8c232591d1ec7eb293e909042d3527132f095103 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 14:08:42 -0700 Subject: [PATCH 11/73] Updated models and endpoints --- .../Service/_MIServiceCredential.swift | 18 +++++ ...layHT.APISpecification.RequestBodies.swift | 71 ++++++++++--------- .../API/PlayHT.APISpecification.swift | 16 +++-- ...yHT.APISpeicification.ResponseBodies.swift | 56 ++++++++++++++- .../Intramodular/Models/PlayHT.Voice.swift | 63 ++++++++-------- .../PlayHT/Intramodular/PlayHT.Client.swift | 59 ++++++--------- .../PlayHT/Intramodular/PlayHT.Model.swift | 3 + 7 files changed, 171 insertions(+), 115 deletions(-) diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift index bbb2ae3a..8873349c 100644 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift +++ b/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift @@ -7,6 +7,7 @@ import Swift public enum _MIServiceCredentialType: String, PersistentIdentifier { case apiKey = "apiKey" + case userIDAndAPIKey = "userIDAndAPIKey" } public protocol _MIServiceCredential: Codable, Hashable, Sendable { @@ -24,3 +25,20 @@ public struct _MIServiceAPIKeyCredential: _MIServiceCredential { self.apiKey = apiKey } } + +public struct _MIServiceUserIDAndAPIKeyCredential: _MIServiceCredential { + public var credentialType: _MIServiceCredentialType { + _MIServiceCredentialType.userIDAndAPIKey + } + + public let userID: String + public let apiKey: String + + public init( + userID: String, + apiKey: String + ) { + self.userID = userID + self.apiKey = apiKey + } +} diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index c568651d..11bf53a3 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -17,27 +17,28 @@ extension PlayHT.APISpecification { public let voiceEngine: PlayHT.Model public let quality: String public let outputFormat: String - public let speed: Double? - public let sampleRate: Int? - public let seed: Int? - public let temperature: Double? - public let emotion: String? - public let voiceGuidance: Double? - public let styleGuidance: Double? - public let textGuidance: Double? - public let language: String? +// public let speed: Double? +// public let sampleRate: Int? +// public let seed: Int? +// public let temperature: Double? +// public let emotion: String? +// public let voiceGuidance: Double? +// public let styleGuidance: Double? +// public let textGuidance: Double? +// public let language: String? +// private enum CodingKeys: String, CodingKey { case text, voice, quality case voiceEngine = "voice_engine" case outputFormat = "output_format" - case speed - case sampleRate = "sample_rate" - case seed, temperature, emotion - case voiceGuidance = "voice_guidance" - case styleGuidance = "style_guidance" - case textGuidance = "text_guidance" - case language +// case speed +// case sampleRate = "sample_rate" +// case seed, temperature, emotion +// case voiceGuidance = "voice_guidance" +// case styleGuidance = "style_guidance" +// case textGuidance = "text_guidance" +// case language } public init( @@ -45,31 +46,31 @@ extension PlayHT.APISpecification { voice: String, voiceEngine: PlayHT.Model = .play3Mini, quality: String = "medium", - outputFormat: String = "mp3", - speed: Double? = nil, - sampleRate: Int? = 48000, - seed: Int? = nil, - temperature: Double? = nil, - emotion: String? = nil, - voiceGuidance: Double? = nil, - styleGuidance: Double? = nil, - textGuidance: Double? = nil, - language: String? = nil + outputFormat: String = "mp3" +// speed: Double? = nil, +// sampleRate: Int? = 48000, +// seed: Int? = nil, +// temperature: Double? = nil, +// emotion: String? = nil, +// voiceGuidance: Double? = nil, +// styleGuidance: Double? = nil, +// textGuidance: Double? = nil, +// language: String? = nil ) { self.text = text self.voice = voice self.voiceEngine = voiceEngine self.quality = quality self.outputFormat = outputFormat - self.speed = speed - self.sampleRate = sampleRate - self.seed = seed - self.temperature = temperature - self.emotion = emotion - self.voiceGuidance = voiceGuidance - self.styleGuidance = styleGuidance - self.textGuidance = textGuidance - self.language = language +// self.speed = speed +// self.sampleRate = sampleRate +// self.seed = seed +// self.temperature = temperature +// self.emotion = emotion +// self.voiceGuidance = voiceGuidance +// self.styleGuidance = styleGuidance +// self.textGuidance = textGuidance +// self.language = language } } diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index dddaa30d..a7b247d2 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -67,11 +67,11 @@ extension PlayHT { @Path("/voices") var listVoices = Endpoint() - // Text to speech - @POST - @Path("/tts") - @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var textToSpeech = Endpoint() + @GET + @Path({ context -> String in + "/tts/\(context.input)" + }) + var checkJobStatus = Endpoint() // Stream text to speech @POST @@ -110,9 +110,9 @@ extension PlayHT.APISpecification { ) request = request - .header("x-api-key", context.root.configuration.apiKey) + .header("X-USER-ID", context.root.configuration.userId) .header("accept", "application/json") - .header("AUTHORIZATION", context.root.configuration.userId) + .header("AUTHORIZATION", context.root.configuration.apiKey) .header(.contentType(.json)) return request @@ -122,6 +122,8 @@ extension PlayHT.APISpecification { from response: Request.Response, context: DecodeOutputContext ) throws -> Output { + print(response) + do { try response.validate() } catch { diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift index d462768c..62a099aa 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift @@ -11,11 +11,63 @@ extension PlayHT.APISpecification { public enum ResponseBodies { public struct Voices: Codable { public let voices: [PlayHT.Voice] + + // Add custom decoding if the response structure is different + public init(from decoder: Decoder) throws { + // If the response is an array directly + if let container = try? decoder.singleValueContainer(), + let voices = try? container.decode([PlayHT.Voice].self) { + self.voices = voices + } else { + // Try decoding as an object with a voices key + let container = try decoder.container(keyedBy: CodingKeys.self) + self.voices = try container.decode([PlayHT.Voice].self, forKey: .voices) + } + } + + private enum CodingKeys: String, CodingKey { + case voices + } } public struct TextToSpeechOutput: Codable { - public let transcriptionId: String - public let audioUrl: String? + public let id: String + public let status: String + public let created: String + public let input: TextToSpeechInput + public let output: String? + public let links: [Link] + + private enum CodingKeys: String, CodingKey { + case id, status, created, input, output + case links = "_links" + } + + public struct TextToSpeechInput: Codable { + public let speed: Double + public let outputFormat: String + public let sampleRate: Int + public let seed: Int? + public let temperature: Double? + public let text: String + public let voice: String + public let quality: String + + private enum CodingKeys: String, CodingKey { + case speed + case outputFormat = "output_format" + case sampleRate = "sample_rate" + case seed, temperature, text, voice, quality + } + } + + public struct Link: Codable { + public let rel: String + public let method: String + public let contentType: String + public let description: String + public let href: String + } } public struct ClonedVoiceOutput: Codable { diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift index 6fc6c91f..aa84909f 100644 --- a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift @@ -6,7 +6,7 @@ // import Foundation -import ElevenLabs +import Swallow extension PlayHT { public struct Voice: Codable, Hashable, Identifiable { @@ -15,19 +15,41 @@ extension PlayHT { public let id: ID public let name: String public let language: String - public let voiceEngine: Model + public let languageCode: String // Add this to store the language code separately + public let voiceEngine: String // Changed from Model to String since it's consistently "PlayHT2.0" public let isCloned: Bool public let gender: String? public let accent: String? public let age: String? public let style: String? - public let useCase: String? - + public let sample: String? // Add this for the sample URL + public let texture: String? // Add these additional fields that appear in the response + public let loudness: String? + public let tempo: String? + private enum CodingKeys: String, CodingKey { - case id, name, language, gender, accent, age, style - case voiceEngine = "voice_engine" - case isCloned = "cloned" - case useCase = "use_case" + case id, name, language, languageCode, voiceEngine, isCloned + case gender, accent, age, style, sample, texture, loudness, tempo + } + + // Add custom decoding if needed to handle any special cases + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try ID(rawValue: container.decode(String.self, forKey: .id)) + self.name = try container.decode(String.self, forKey: .name) + self.language = try container.decode(String.self, forKey: .language) + self.languageCode = try container.decode(String.self, forKey: .languageCode) + self.voiceEngine = try container.decode(String.self, forKey: .voiceEngine) + self.isCloned = try container.decode(Bool.self, forKey: .isCloned) + self.gender = try container.decodeIfPresent(String.self, forKey: .gender) + self.accent = try container.decodeIfPresent(String.self, forKey: .accent) + self.age = try container.decodeIfPresent(String.self, forKey: .age) + self.style = try container.decodeIfPresent(String.self, forKey: .style) + self.sample = try container.decodeIfPresent(String.self, forKey: .sample) + self.texture = try container.decodeIfPresent(String.self, forKey: .texture) + self.loudness = try container.decodeIfPresent(String.self, forKey: .loudness) + self.tempo = try container.decodeIfPresent(String.self, forKey: .tempo) } } @@ -47,28 +69,3 @@ extension PlayHT { case flac = "flac" } } - -#warning("This is only a temporary fix. Remove these & replace with Abstract (@jared)") - -extension PlayHT.Voice { - public func toElevenLabsVoice() -> ElevenLabs.Voice { - ElevenLabs.Voice( - voiceID: id.rawValue, - name: name, - description: style, - isOwner: isCloned - ) - } -} - -extension PlayHT.VoiceSettings { - public static func fromElevenLabs(_ settings: ElevenLabs.VoiceSettings) -> Self { - PlayHT.VoiceSettings( - speed: 1.0, - temperature: settings.stability, - voiceGuidance: settings.similarityBoost * 6.0, - styleGuidance: settings.styleExaggeration * 30.0, - textGuidance: 1.0 + settings.similarityBoost - ) - } -} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 1ee76dc5..e2944714 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -31,8 +31,8 @@ extension PlayHT { self.sessionCache = .init() } - public convenience init(apiKey: String) { - self.init(configuration: .init(apiKey: apiKey)) + public convenience init(apiKey: String, userID: String) { + self.init(configuration: .init(apiKey: apiKey, userId: userID)) } } } @@ -48,16 +48,16 @@ extension PlayHT.Client: _MIService { throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { + guard let credential = account.credential as? _MIServiceUserIDAndAPIKeyCredential else { throw _MIServiceError.invalidCredentials(account.credential) } - self.init(apiKey: credential.apiKey) + self.init(apiKey: credential.apiKey, userID: credential.userID) } } extension PlayHT.Client { - public func availableVoices() async throws -> [PlayHT.Voice] { + public func playHTAvailableVoices() async throws -> [PlayHT.Voice] { try await run(\.listVoices).voices } @@ -67,48 +67,37 @@ extension PlayHT.Client { public func generateSpeech( text: String, - voice: PlayHT.Voice, + voice: String, settings: PlayHT.VoiceSettings, - outputSettings: PlayHT.OutputSettings = .default - ) async throws -> String? { + outputSettings: PlayHT.OutputSettings = .default, + model: PlayHT.Model + ) async throws -> Data { let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, - voice: voice.id.rawValue, - voiceEngine: voice.voiceEngine, + voice: voice, + voiceEngine: model, quality: outputSettings.quality.rawValue, - outputFormat: outputSettings.format.rawValue, - speed: settings.speed, - sampleRate: outputSettings.sampleRate, - temperature: settings.temperature, - voiceGuidance: settings.voiceGuidance, - styleGuidance: settings.styleGuidance, - textGuidance: settings.textGuidance, - language: voice.language + outputFormat: outputSettings.format.rawValue ) - let response = try await run(\.textToSpeech, with: input) - return response.audioUrl + let response = try await run(\.streamTextToSpeech, with: input) + print(response) + return response } public func streamSpeech( text: String, - voice: PlayHT.Voice, + voice: String, settings: PlayHT.VoiceSettings, - outputSettings: PlayHT.OutputSettings = .default + outputSettings: PlayHT.OutputSettings = .default, + model: PlayHT.Model ) async throws -> Data { let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, - voice: voice.id.rawValue, - voiceEngine: voice.voiceEngine, + voice: voice, + voiceEngine: model, quality: outputSettings.quality.rawValue, - outputFormat: outputSettings.format.rawValue, - speed: settings.speed, - sampleRate: outputSettings.sampleRate, - temperature: settings.temperature, - voiceGuidance: settings.voiceGuidance, - styleGuidance: settings.styleGuidance, - textGuidance: settings.textGuidance, - language: voice.language + outputFormat: outputSettings.format.rawValue ) return try await run(\.streamTextToSpeech, with: input) @@ -132,10 +121,4 @@ extension PlayHT.Client { ) async throws { try await run(\.deleteClonedVoice, with: voice.rawValue) } - - private func findVoice(id: String) async throws -> PlayHT.Voice? { - let allVoices = try await availableVoices() - let clonedVoices = try await clonedVoices() - return (allVoices + clonedVoices).first { $0.id.rawValue == id } - } } diff --git a/Sources/PlayHT/Intramodular/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/PlayHT.Model.swift index 560f7643..c8427959 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Model.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Model.swift @@ -20,6 +20,9 @@ extension PlayHT { /// - 20k character limit per stream case play3Mini = "Play3.0-mini" + case playHT2 = "PlayHT2.0" + case playHT1 = "PlayHT1.0" + /// Legacy voice model with basic TTS capabilities case playHT2Turbo = "PlayHT2.0-turbo" } From 445485a1ddfece6edb99a03232c51aa64e237e83 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 15:07:43 -0700 Subject: [PATCH 12/73] updated client --- .../PlayHT/Intramodular/PlayHT.Client.swift | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index e2944714..462bbfd0 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -65,13 +65,14 @@ extension PlayHT.Client { try await run(\.listClonedVoices).voices } - public func generateSpeech( + public func streamTextToSpeech( text: String, voice: String, settings: PlayHT.VoiceSettings, outputSettings: PlayHT.OutputSettings = .default, model: PlayHT.Model ) async throws -> Data { + // Construct the input for the API let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, voice: voice, @@ -80,29 +81,37 @@ extension PlayHT.Client { outputFormat: outputSettings.format.rawValue ) - let response = try await run(\.streamTextToSpeech, with: input) - print(response) - return response - } - - public func streamSpeech( - text: String, - voice: String, - settings: PlayHT.VoiceSettings, - outputSettings: PlayHT.OutputSettings = .default, - model: PlayHT.Model - ) async throws -> Data { - let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( - text: text, - voice: voice, - voiceEngine: model, - quality: outputSettings.quality.rawValue, - outputFormat: outputSettings.format.rawValue - ) + // Fetch the initial JSON response + let responseData = try await run(\.streamTextToSpeech, with: input) + + // Decode the response to extract the audio URL + let audioResponse = try JSONDecoder().decode(PlayHT.Client.AudioResponse.self, from: responseData) + + guard let audioUrl = URL(string: audioResponse.href) else { + throw PlayHTError.invalidURL + } + + #warning("This should be cleaned up @jared") + var request = URLRequest(url: audioUrl) + request.httpMethod = "GET" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue(interface.configuration.userId ?? "", forHTTPHeaderField: "X-USER-ID") + request.addValue(interface.configuration.apiKey ?? "", forHTTPHeaderField: "AUTHORIZATION") + + let (audioData, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw PlayHTError.audioFetchFailed + } - return try await run(\.streamTextToSpeech, with: input) + guard !audioData.isEmpty else { + throw PlayHTError.audioFetchFailed + } + + return audioData } + public func instantCloneVoice( sampleFileURL: String, name: String @@ -122,3 +131,28 @@ extension PlayHT.Client { try await run(\.deleteClonedVoice, with: voice.rawValue) } } + +extension PlayHT.Client { + enum PlayHTError: LocalizedError { + case invalidURL + case audioFetchFailed + + var errorDescription: String? { + switch self { + case .invalidURL: + return "Invalid audio URL received from PlayHT" + case .audioFetchFailed: + return "Failed to fetch audio data from PlayHT" + } + } + } +} +extension PlayHT.Client { + public struct AudioResponse: Codable { + public let description: String + public let method: String + public let href: String + public let contentType: String + public let rel: String + } +} From 6cb09e1b473db2d258cbea569f2ea1ae4289bd40 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 17:20:26 -0700 Subject: [PATCH 13/73] Deleting Voice --- .../API/ElevenLabs.APISpecification.swift | 4 ---- ...layHT.APISpecification.RequestBodies.swift | 24 +++++++++++++++---- .../API/PlayHT.APISpecification.swift | 19 ++++----------- .../Intramodular/Models/PlayHT.Voice.swift | 21 +++++++++------- .../PlayHT/Intramodular/PlayHT.Client.swift | 11 ++++++++- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift index ce9e2742..fa442c34 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift @@ -167,10 +167,6 @@ extension ElevenLabs.APISpecification { return response.data as! Output } - if Input.self == RequestBodies.EditVoiceInput.self { - print(response) - } - return try response.decode( Output.self, keyDecodingStrategy: .convertFromSnakeCase diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index 11bf53a3..d668c773 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -74,6 +74,14 @@ extension PlayHT.APISpecification { } } + public struct DeleteVoiceInput: Codable, Hashable { + var voiceID: String + + enum CodingKeys: String, CodingKey { + case voiceID = "voice_id" + } + } + public struct InstantCloneVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { public let sampleFileURL: String public let voiceName: String @@ -86,12 +94,18 @@ extension PlayHT.APISpecification { public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() - result.append( - .text( - named: "sample_file_url", - value: sampleFileURL + if let url = URL(string: sampleFileURL), + let fileData = try? Data(contentsOf: url) { + print(fileData) + result.append( + .file( + named: "sample_file", + data: fileData, + filename: url.lastPathComponent, + contentType: .mp4 + ) ) - ) + } result.append( .text( diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index a7b247d2..8012abbd 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -66,13 +66,7 @@ extension PlayHT { @GET @Path("/voices") var listVoices = Endpoint() - - @GET - @Path({ context -> String in - "/tts/\(context.input)" - }) - var checkJobStatus = Endpoint() - + // Stream text to speech @POST @Path("/tts/stream") @@ -80,7 +74,7 @@ extension PlayHT { var streamTextToSpeech = Endpoint() // Clone Voice - @POST + @GET @Path("/cloned-voices") var listClonedVoices = Endpoint() @@ -91,10 +85,9 @@ extension PlayHT { // Delete cloned voice @DELETE - @Path({ context -> String in - "/cloned-voices/\(context.input)" - }) - var deleteClonedVoice = Endpoint() + @Path("/cloned-voices") + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) + var deleteClonedVoice = Endpoint() } } @@ -122,8 +115,6 @@ extension PlayHT.APISpecification { from response: Request.Response, context: DecodeOutputContext ) throws -> Output { - print(response) - do { try response.validate() } catch { diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift index aa84909f..3ac8907f 100644 --- a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift @@ -14,16 +14,16 @@ extension PlayHT { public let id: ID public let name: String - public let language: String - public let languageCode: String // Add this to store the language code separately - public let voiceEngine: String // Changed from Model to String since it's consistently "PlayHT2.0" - public let isCloned: Bool + public let language: String? + public let languageCode: String? + public let voiceEngine: String + public let isCloned: Bool? public let gender: String? public let accent: String? public let age: String? public let style: String? - public let sample: String? // Add this for the sample URL - public let texture: String? // Add these additional fields that appear in the response + public let sample: String? + public let texture: String? public let loudness: String? public let tempo: String? @@ -38,10 +38,13 @@ extension PlayHT { self.id = try ID(rawValue: container.decode(String.self, forKey: .id)) self.name = try container.decode(String.self, forKey: .name) - self.language = try container.decode(String.self, forKey: .language) - self.languageCode = try container.decode(String.self, forKey: .languageCode) + self.language = try container.decodeIfPresent(String.self, forKey: .language) + self.languageCode = try container.decodeIfPresent(String.self, forKey: .languageCode) self.voiceEngine = try container.decode(String.self, forKey: .voiceEngine) - self.isCloned = try container.decode(Bool.self, forKey: .isCloned) + + self.isCloned = try container.decodeIfPresent(Bool.self, forKey: .isCloned) ?? true + // isCloned is always false if not created by user. Otherwise doesn't exist so we set to true + self.gender = try container.decodeIfPresent(String.self, forKey: .gender) self.accent = try container.decodeIfPresent(String.self, forKey: .accent) self.age = try container.decodeIfPresent(String.self, forKey: .age) diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 462bbfd0..5568bc92 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -57,6 +57,15 @@ extension PlayHT.Client: _MIService { } extension PlayHT.Client { + + public func getAllAvailableVoices() async throws -> [PlayHT.Voice] { + async let htVoices = playHTAvailableVoices() + async let clonedVoices = clonedVoices() + + let (available, cloned) = try await (htVoices, clonedVoices) + return available + cloned + } + public func playHTAvailableVoices() async throws -> [PlayHT.Voice] { try await run(\.listVoices).voices } @@ -128,7 +137,7 @@ extension PlayHT.Client { public func deleteClonedVoice( voice: PlayHT.Voice.ID ) async throws { - try await run(\.deleteClonedVoice, with: voice.rawValue) + try await run(\.deleteClonedVoice, with: .init(voiceID: voice.rawValue)) } } From 3240a4c366c76d44cb12fabe2ab54b39732a9c48 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 17:55:52 -0700 Subject: [PATCH 14/73] cleanup --- ...layHT.APISpecification.RequestBodies.swift | 77 +++++++++---------- .../API/PlayHT.APISpecification.swift | 6 +- ...yHT.APISpeicification.ResponseBodies.swift | 55 +++---------- .../PlayHT/Intramodular/PlayHT.Client.swift | 20 +---- .../PlayHT/Intramodular/PlayHT.Model.swift | 9 +-- 5 files changed, 55 insertions(+), 112 deletions(-) diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index d668c773..8aa73eda 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -18,59 +18,59 @@ extension PlayHT.APISpecification { public let quality: String public let outputFormat: String -// public let speed: Double? -// public let sampleRate: Int? -// public let seed: Int? -// public let temperature: Double? -// public let emotion: String? -// public let voiceGuidance: Double? -// public let styleGuidance: Double? -// public let textGuidance: Double? -// public let language: String? -// + // public let speed: Double? + // public let sampleRate: Int? + // public let seed: Int? + // public let temperature: Double? + // public let emotion: String? + // public let voiceGuidance: Double? + // public let styleGuidance: Double? + // public let textGuidance: Double? + // public let language: String? + // private enum CodingKeys: String, CodingKey { case text, voice, quality case voiceEngine = "voice_engine" case outputFormat = "output_format" -// case speed -// case sampleRate = "sample_rate" -// case seed, temperature, emotion -// case voiceGuidance = "voice_guidance" -// case styleGuidance = "style_guidance" -// case textGuidance = "text_guidance" -// case language + // case speed + // case sampleRate = "sample_rate" + // case seed, temperature, emotion + // case voiceGuidance = "voice_guidance" + // case styleGuidance = "style_guidance" + // case textGuidance = "text_guidance" + // case language } public init( text: String, voice: String, - voiceEngine: PlayHT.Model = .play3Mini, + voiceEngine: PlayHT.Model = .playHT2, quality: String = "medium", outputFormat: String = "mp3" -// speed: Double? = nil, -// sampleRate: Int? = 48000, -// seed: Int? = nil, -// temperature: Double? = nil, -// emotion: String? = nil, -// voiceGuidance: Double? = nil, -// styleGuidance: Double? = nil, -// textGuidance: Double? = nil, -// language: String? = nil + // speed: Double? = nil, + // sampleRate: Int? = 48000, + // seed: Int? = nil, + // temperature: Double? = nil, + // emotion: String? = nil, + // voiceGuidance: Double? = nil, + // styleGuidance: Double? = nil, + // textGuidance: Double? = nil, + // language: String? = nil ) { self.text = text self.voice = voice self.voiceEngine = voiceEngine self.quality = quality self.outputFormat = outputFormat -// self.speed = speed -// self.sampleRate = sampleRate -// self.seed = seed -// self.temperature = temperature -// self.emotion = emotion -// self.voiceGuidance = voiceGuidance -// self.styleGuidance = styleGuidance -// self.textGuidance = textGuidance -// self.language = language + // self.speed = speed + // self.sampleRate = sampleRate + // self.seed = seed + // self.temperature = temperature + // self.emotion = emotion + // self.voiceGuidance = voiceGuidance + // self.styleGuidance = styleGuidance + // self.textGuidance = textGuidance + // self.language = language } } @@ -92,11 +92,10 @@ extension PlayHT.APISpecification { } public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() + var result: HTTPRequest.Multipart.Content = .init() - if let url = URL(string: sampleFileURL), + if let url: URL = URL(string: sampleFileURL), let fileData = try? Data(contentsOf: url) { - print(fileData) result.append( .file( named: "sample_file", diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index 8012abbd..f478bd72 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -71,13 +71,13 @@ extension PlayHT { @POST @Path("/tts/stream") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var streamTextToSpeech = Endpoint() + var streamTextToSpeech = Endpoint() - // Clone Voice @GET @Path("/cloned-voices") var listClonedVoices = Endpoint() + // Clone Voice @POST @Path("/cloned-voices/instant") @Body(multipart: .input) @@ -97,7 +97,7 @@ extension PlayHT.APISpecification { from input: Input, context: BuildRequestContext ) throws -> Request { - var request = try super.buildRequestBase( + var request: HTTPRequest = try super.buildRequestBase( from: input, context: context ) diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift index 62a099aa..12b2b8b1 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpeicification.ResponseBodies.swift @@ -12,14 +12,11 @@ extension PlayHT.APISpecification { public struct Voices: Codable { public let voices: [PlayHT.Voice] - // Add custom decoding if the response structure is different public init(from decoder: Decoder) throws { - // If the response is an array directly - if let container = try? decoder.singleValueContainer(), - let voices = try? container.decode([PlayHT.Voice].self) { + if let container: SingleValueDecodingContainer = try? decoder.singleValueContainer(), + let voices: [PlayHT.Voice] = try? container.decode([PlayHT.Voice].self) { self.voices = voices } else { - // Try decoding as an object with a voices key let container = try decoder.container(keyedBy: CodingKeys.self) self.voices = try container.decode([PlayHT.Voice].self, forKey: .voices) } @@ -30,50 +27,18 @@ extension PlayHT.APISpecification { } } - public struct TextToSpeechOutput: Codable { - public let id: String - public let status: String - public let created: String - public let input: TextToSpeechInput - public let output: String? - public let links: [Link] - - private enum CodingKeys: String, CodingKey { - case id, status, created, input, output - case links = "_links" - } - - public struct TextToSpeechInput: Codable { - public let speed: Double - public let outputFormat: String - public let sampleRate: Int - public let seed: Int? - public let temperature: Double? - public let text: String - public let voice: String - public let quality: String - - private enum CodingKeys: String, CodingKey { - case speed - case outputFormat = "output_format" - case sampleRate = "sample_rate" - case seed, temperature, text, voice, quality - } - } - - public struct Link: Codable { - public let rel: String - public let method: String - public let contentType: String - public let description: String - public let href: String - } - } - public struct ClonedVoiceOutput: Codable { public let id: String public let name: String public let status: String } + + public struct TextToSpeechResponse: Codable { + public let description: String + public let method: String + public let href: String + public let contentType: String + public let rel: String + } } } diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 5568bc92..f074c9d0 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -81,7 +81,7 @@ extension PlayHT.Client { outputSettings: PlayHT.OutputSettings = .default, model: PlayHT.Model ) async throws -> Data { - // Construct the input for the API + let input = PlayHT.APISpecification.RequestBodies.TextToSpeechInput( text: text, voice: voice, @@ -90,18 +90,13 @@ extension PlayHT.Client { outputFormat: outputSettings.format.rawValue ) - // Fetch the initial JSON response let responseData = try await run(\.streamTextToSpeech, with: input) - // Decode the response to extract the audio URL - let audioResponse = try JSONDecoder().decode(PlayHT.Client.AudioResponse.self, from: responseData) - - guard let audioUrl = URL(string: audioResponse.href) else { + guard let url = URL(string: responseData.href) else { throw PlayHTError.invalidURL } - #warning("This should be cleaned up @jared") - var request = URLRequest(url: audioUrl) + var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(interface.configuration.userId ?? "", forHTTPHeaderField: "X-USER-ID") @@ -156,12 +151,3 @@ extension PlayHT.Client { } } } -extension PlayHT.Client { - public struct AudioResponse: Codable { - public let description: String - public let method: String - public let href: String - public let contentType: String - public let rel: String - } -} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/PlayHT.Model.swift index c8427959..9eeb0a28 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Model.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Model.swift @@ -12,18 +12,11 @@ import Swift extension PlayHT { public enum Model: String, Codable, Sendable { - /// Latest speech model optimized for realtime use. Features include: - /// - Multilingual support (36 languages) - /// - Reduced hallucinations - /// - <200ms streaming latency - /// - 48kHz sampling - /// - 20k character limit per stream - case play3Mini = "Play3.0-mini" case playHT2 = "PlayHT2.0" + case playHT1 = "PlayHT1.0" - /// Legacy voice model with basic TTS capabilities case playHT2Turbo = "PlayHT2.0-turbo" } } From 94f67d80e0c6e1bdec3d6b770f092bc88bde78ac Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 20 Nov 2024 18:14:50 -0700 Subject: [PATCH 15/73] removed tests --- Tests/PlayHT/Intramodular/SpeechTests.swift | 61 --------------------- Tests/PlayHT/module.swift | 13 ++++- 2 files changed, 10 insertions(+), 64 deletions(-) diff --git a/Tests/PlayHT/Intramodular/SpeechTests.swift b/Tests/PlayHT/Intramodular/SpeechTests.swift index e4b58291..8647aad5 100644 --- a/Tests/PlayHT/Intramodular/SpeechTests.swift +++ b/Tests/PlayHT/Intramodular/SpeechTests.swift @@ -10,65 +10,4 @@ import XCTest final class PlayHTTests: XCTestCase { - let sampleText = "In a quiet, unassuming village nestled deep in a lush, verdant valley, young Elara leads a simple life, dreaming of adventure beyond the horizon. Her village is filled with ancient folklore and tales of mystical relics, but none capture her imagination like the legend of the Enchanted Amulet—a powerful artifact said to grant its bearer the ability to control time." - - func testListVoices() async throws { - let voices = try await client.availableVoices() - XCTAssertFalse(voices.isEmpty) - } - - func testCreateSpeech() async throws { - let voices = try await client.availableVoices() - let voice = try XCTUnwrap(voices.first) - - let voiceSettings = PlayHT.VoiceSettings( - speed: 1.0, - temperature: 0.7, - voiceGuidance: 3.0, - styleGuidance: 15.0, - textGuidance: 1.5 - ) - - let outputSettings = PlayHT.OutputSettings( - quality: .high, - format: .mp3, - sampleRate: 48000 - ) - - let audioURL = try await client.generateSpeech( - text: sampleText, - voice: voice, - settings: voiceSettings, - outputSettings: outputSettings - ) - - XCTAssertNotNil(audioURL) - } - - func testStreamSpeech() async throws { - let voices = try await client.availableVoices() - let voice = try XCTUnwrap(voices.first) - - let voiceSettings = PlayHT.VoiceSettings() - let outputSettings = PlayHT.OutputSettings() - - let data = try await client.streamSpeech( - text: sampleText, - voice: voice, - settings: voiceSettings, - outputSettings: outputSettings - ) - - XCTAssertFalse(data.isEmpty) - } - - func testInstantCloneVoice() async throws { - let sampleURL = "https://example.com/sample.mp3" - let voiceID = try await client.instantCloneVoice( - sampleFileURL: sampleURL, - name: "Test Clone" - ) - - XCTAssertNotNil(voiceID) - } } diff --git a/Tests/PlayHT/module.swift b/Tests/PlayHT/module.swift index c217577d..182cfdab 100644 --- a/Tests/PlayHT/module.swift +++ b/Tests/PlayHT/module.swift @@ -2,14 +2,21 @@ // Copyright (c) Vatsal Manot // -import ElevenLabs +import PlayHT public var PLAYHT_API_KEY: String { - "0dea648f8b5c9497b647902ae00e6903" + "fcfc923b8bd44fc383c9d23e409d52b1" +} + +public var PLAYHT_USER_ID: String { + "gze0b6x9kbXPVPOINZTAB09TsZ63" } public var client: PlayHT.Client { - let client = PlayHT.Client(apiKey: PLAYHT_API_KEY) + let client = PlayHT.Client( + apiKey: PLAYHT_API_KEY, + userID: PLAYHT_USER_ID + ) return client } From c88a31b62b29f32819a427993ac7b288859e626d Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 13:43:19 -0700 Subject: [PATCH 16/73] initial --- .../Model Identifier/ModelIdentifier.Provider.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index 6917b038..b8594f73 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -25,6 +25,7 @@ extension ModelIdentifier { case _ElevenLabs case _TogetherAI case _PlayHT + case _Rime case unknown(String) @@ -79,6 +80,10 @@ extension ModelIdentifier { public static var playHT: Self { Self._PlayHT } + + public static var rime: Self { + Self._Rime + } } } @@ -117,6 +122,8 @@ extension ModelIdentifier.Provider: CustomStringConvertible { return "TogetherAI" case ._PlayHT: return "PlayHT" + case ._Rime: + return "Rime" case .unknown(let provider): return provider } From f5dc921e71842e8022b9d10efd8a6a207b494d69 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 13:58:06 -0700 Subject: [PATCH 17/73] Added identifiers --- .../Model Identifier/ModelIdentifier.Provider.swift | 4 ++++ .../Intramodular/Service/_MIServiceTypeIdentifier.swift | 1 + 2 files changed, 5 insertions(+) diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index b8594f73..126ac3ad 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -163,6 +163,8 @@ extension ModelIdentifier.Provider: RawRepresentable { return "togetherai" case ._PlayHT: return "playht" + case ._Rime: + return "rime" case .unknown(let provider): return provider } @@ -198,6 +200,8 @@ extension ModelIdentifier.Provider: RawRepresentable { self = ._TogetherAI case Self._PlayHT.rawValue: self = ._PlayHT + case Self._Rime.rawValue: + self = ._Rime default: self = .unknown(rawValue) } diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift index e7ffab25..babec233 100644 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift +++ b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift @@ -39,4 +39,5 @@ extension _MIServiceTypeIdentifier { public static let _Cohere = _MIServiceTypeIdentifier(rawValue: "guzob-fipin-navij-duvon") public static let _TogetherAI = _MIServiceTypeIdentifier(rawValue: "pafob-vopoj-lurig-zilur") public static let _PlayHT = _MIServiceTypeIdentifier(rawValue: "foluv-jufuk-zuhok-hofid") + public static let _Rime = _MIServiceTypeIdentifier(rawValue: "tohaz-zivir-bosov-minog") } From ac86273939a4647de74f1e9636f24d77069f5898 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 14:08:48 -0700 Subject: [PATCH 18/73] Files --- .../Rime.APISpecification.RequestBodies.swift | 7 +++++++ ...Rime.APISpecification.ResponseBodies.swift | 7 +++++++ .../API/Rime.APISpecification.swift | 7 +++++++ Sources/Rime/Intramodular/Rime.Client.swift | 19 +++++++++++++++++++ Sources/Rime/Intramodular/Rime.Model.swift | 7 +++++++ Sources/Rime/Intramodular/Rime.swift | 12 ++++++++++++ Sources/Rime/module.swift | 7 +++++++ 7 files changed, 66 insertions(+) create mode 100644 Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift create mode 100644 Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift create mode 100644 Sources/Rime/Intramodular/API/Rime.APISpecification.swift create mode 100644 Sources/Rime/Intramodular/Rime.Client.swift create mode 100644 Sources/Rime/Intramodular/Rime.Model.swift create mode 100644 Sources/Rime/Intramodular/Rime.swift create mode 100644 Sources/Rime/module.swift diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..e01779af --- /dev/null +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift @@ -0,0 +1,7 @@ +// +// Rime.APISpecification.RequestBodies.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift new file mode 100644 index 00000000..3283d02f --- /dev/null +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -0,0 +1,7 @@ +// +// Rime.APISpecification.ResponseBodies.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift new file mode 100644 index 00000000..a685f4c3 --- /dev/null +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift @@ -0,0 +1,7 @@ +// +// Rime.APISpecification.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift new file mode 100644 index 00000000..16295a6d --- /dev/null +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -0,0 +1,19 @@ +// +// Rime.Client.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension Rime { + @RuntimeDiscoverable + public final class Client: HTTPClient, _StaticSwift.Namespace { + + } +} diff --git a/Sources/Rime/Intramodular/Rime.Model.swift b/Sources/Rime/Intramodular/Rime.Model.swift new file mode 100644 index 00000000..e1c418ca --- /dev/null +++ b/Sources/Rime/Intramodular/Rime.Model.swift @@ -0,0 +1,7 @@ +// +// Rime.Model.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + diff --git a/Sources/Rime/Intramodular/Rime.swift b/Sources/Rime/Intramodular/Rime.swift new file mode 100644 index 00000000..ad0ae787 --- /dev/null +++ b/Sources/Rime/Intramodular/Rime.swift @@ -0,0 +1,12 @@ +// +// Rime.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + +import Swift + +public enum Rime { + +} diff --git a/Sources/Rime/module.swift b/Sources/Rime/module.swift new file mode 100644 index 00000000..c7efeae6 --- /dev/null +++ b/Sources/Rime/module.swift @@ -0,0 +1,7 @@ +// +// module.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + From 54bccbda8d745d027ae6d639ca94479cade8a89f Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 14:09:43 -0700 Subject: [PATCH 19/73] HTTPClient --- Sources/PlayHT/Intramodular/PlayHT.Client.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index f074c9d0..41a370bf 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -13,7 +13,7 @@ import Swallow extension PlayHT { @RuntimeDiscoverable - public final class Client: SwiftAPI.Client, ObservableObject { + public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { _MIServiceTypeIdentifier._PlayHT } From e907dd2b99ea0243f929810b3ae0625b1eadce57 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 14:53:37 -0700 Subject: [PATCH 20/73] get all voices --- Package.swift | 18 ++- .../Rime.APISpecification.RequestBodies.swift | 9 ++ ...Rime.APISpecification.ResponseBodies.swift | 25 ++++ .../API/Rime.APISpecification.swift | 114 ++++++++++++++++++ .../Rime/Intramodular/Models/Rime.Voice.swift | 56 +++++++++ Sources/Rime/Intramodular/Rime.Client.swift | 44 +++++++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 Sources/Rime/Intramodular/Models/Rime.Voice.swift diff --git a/Package.swift b/Package.swift index 6c3f83ab..1cb9aa05 100644 --- a/Package.swift +++ b/Package.swift @@ -120,6 +120,21 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .target( + name: "Rime", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/Rime", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .target( name: "_Gemini", dependencies: [ @@ -294,12 +309,13 @@ let package = Package( "OpenAI", "Perplexity", "PlayHT", + "Rime", "Swallow", "Jina", "VoyageAI", "Cohere", "TogetherAI", - "HuggingFace" + "HuggingFace", ], path: "Sources/AI", swiftSettings: [ diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift index e01779af..be467c69 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift @@ -5,3 +5,12 @@ // Created by Jared Davidson on 11/21/24. // +import NetworkKit +import SwiftAPI +import Merge + +extension Rime.APISpecification { + enum RequestBodies { + + } +} diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index 3283d02f..f3371845 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -5,3 +5,28 @@ // Created by Jared Davidson on 11/21/24. // +import NetworkKit +import SwiftAPI +import Merge + +extension Rime.APISpecification { + enum ResponseBodies { + public struct Voices: Codable { + public let voices: [Rime.Voice] + + public init(voices: [Rime.Voice]) { + self.voices = voices + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.voices = try container.decode([Rime.Voice].self) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(voices) + } + } + } +} diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift index a685f4c3..c689a252 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift @@ -5,3 +5,117 @@ // Created by Jared Davidson on 11/21/24. // +import CorePersistence +import Diagnostics +import NetworkKit +import Swift +import SwiftAPI + +extension Rime { + public enum APIError: APIErrorProtocol { + public typealias API = Rime.APISpecification + + case apiKeyMissing + case userIdMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case invalidContentType + case badRequest(request: API.Request?, error: API.Request.Error) + case unknown(message: String) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var host: URL + public var apiKey: String? + + public init( + host: URL = URL(string: "https://users.rime.ai/")!, + apiKey: String? = nil + ) { + self.host = host + self.apiKey = apiKey + } + } + + public let configuration: Configuration + + public var host: URL { + configuration.host + } + + public var id: some Hashable { + configuration + } + + public init(configuration: Configuration) { + self.configuration = configuration + } + + @GET + @Path("/data/voices/voice_details.json") + var listVoices = Endpoint() + } +} + +extension Rime.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + public override func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + var request: HTTPRequest = try super.buildRequestBase( + from: input, + context: context + ) + + request = request + .header("Accept", "application/json") + .header("Authorization", "Bearer \(context.root.configuration.apiKey ?? "")") + .header(.contentType(.json)) + + return request + } + + public override func decodeOutputBase( + from response: HTTPResponse, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? HTTPRequest.Error { + if response.statusCode.rawValue == 401 { + apiError = .incorrectAPIKeyProvided + } else if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + if Output.self == Data.self { + return response.data as! Output + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} diff --git a/Sources/Rime/Intramodular/Models/Rime.Voice.swift b/Sources/Rime/Intramodular/Models/Rime.Voice.swift new file mode 100644 index 00000000..13ae4e43 --- /dev/null +++ b/Sources/Rime/Intramodular/Models/Rime.Voice.swift @@ -0,0 +1,56 @@ +// +// Rime.Voice.swift +// AI +// +// Created by Jared Davidson on 11/21/24. +// + +import Foundation +import Swallow + +extension Rime { + public struct Voice: Codable, Hashable, Identifiable { + public typealias ID = _TypeAssociatedID + + public let id: ID + public let name: String + public let age: String + public let country: String + public let region: String + public let demographic: String + public let genre: [String] + + enum CodingKeys: CodingKey { + case id + case name + case age + case country + case region + case demographic + case genre + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: Rime.Voice.CodingKeys.self) + self.id = try container.decode(Rime.Voice.ID.self, forKey: Rime.Voice.CodingKeys.id) + self.name = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.name) + self.age = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.age) + self.country = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.country) + self.region = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.region) + self.demographic = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.demographic) + self.genre = try container.decode([String].self, forKey: Rime.Voice.CodingKeys.genre) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: Rime.Voice.CodingKeys.self) + try container.encode(self.id, forKey: Rime.Voice.CodingKeys.id) + try container.encode(self.name, forKey: Rime.Voice.CodingKeys.name) + try container.encode(self.age, forKey: Rime.Voice.CodingKeys.age) + try container.encode(self.country, forKey: Rime.Voice.CodingKeys.country) + try container.encode(self.region, forKey: Rime.Voice.CodingKeys.region) + try container.encode(self.demographic, forKey: Rime.Voice.CodingKeys.demographic) + try container.encode(self.genre, forKey: Rime.Voice.CodingKeys.genre) + } + + } +} diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index 16295a6d..ee0cbb51 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -14,6 +14,50 @@ import Swallow extension Rime { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { + public static var persistentTypeRepresentation: some IdentityRepresentation { + _MIServiceTypeIdentifier._Rime + } + public typealias API = Rime.APISpecification + public typealias Session = HTTPSession + + public let interface: API + public let session: Session + public var sessionCache: EmptyKeyedCache + + public required init(configuration: API.Configuration) { + self.interface = API(configuration: configuration) + self.session = HTTPSession.shared + self.sessionCache = .init() + } + + public convenience init(apiKey: String) { + self.init(configuration: .init(apiKey: apiKey)) + } + } +} + +extension Rime.Client: _MIService { + public convenience init( + account: (any _MIServiceAccount)? + ) async throws { + let account: any _MIServiceAccount = try account.unwrap() + let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + + guard serviceIdentifier == _MIServiceTypeIdentifier._PlayHT else { + throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + } + + guard let credential = account.credential as? _MIServiceAPIKeyCredential else { + throw _MIServiceError.invalidCredentials(account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} + +extension Rime.Client { + public func getAllAvailableVoices() async throws -> [Rime.Voice] { + async let voice } } From 338ee59887dcef18b52181aafea9e53cb8281ebb Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 15:02:16 -0700 Subject: [PATCH 21/73] Model --- Sources/Rime/Intramodular/Rime.Client.swift | 2 +- Sources/Rime/Intramodular/Rime.Model.swift | 41 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index ee0cbb51..1ccc3f10 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -58,6 +58,6 @@ extension Rime.Client: _MIService { extension Rime.Client { public func getAllAvailableVoices() async throws -> [Rime.Voice] { - async let voice + try await run(\.listVoices).voices } } diff --git a/Sources/Rime/Intramodular/Rime.Model.swift b/Sources/Rime/Intramodular/Rime.Model.swift index e1c418ca..cc6e1f24 100644 --- a/Sources/Rime/Intramodular/Rime.Model.swift +++ b/Sources/Rime/Intramodular/Rime.Model.swift @@ -5,3 +5,44 @@ // Created by Jared Davidson on 11/21/24. // +import CoreMI +import CorePersistence +import Foundation +import Swift + +extension Rime { + public enum Model: String, Codable, Sendable { + case mist = "mist" + case v1 = "v1" + } +} + +// MARK: - Conformances + +extension Rime.Model: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension Rime.Model: ModelIdentifierRepresentable { + public init(from identifier: ModelIdentifier) throws { + guard identifier.provider == ._Rime, identifier.revision == nil else { + throw Never.Reason.illegal + } + + guard let model = Self(rawValue: identifier.name) else { + throw Never.Reason.unexpected + } + + self = model + } + + public func __conversion() throws -> ModelIdentifier { + ModelIdentifier( + provider: ._Rime, + name: rawValue, + revision: nil + ) + } +} From 8337564e109324af3b50d288de90f0d07e0cea97 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 15:11:38 -0700 Subject: [PATCH 22/73] text tp speech --- .../Rime.APISpecification.RequestBodies.swift | 17 ++++++++++++++++- .../Rime.APISpecification.ResponseBodies.swift | 18 ++++++++++++++++++ .../API/Rime.APISpecification.swift | 4 ++++ Sources/Rime/Intramodular/Rime.Client.swift | 17 +++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift index be467c69..2d39e1e7 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift @@ -11,6 +11,21 @@ import Merge extension Rime.APISpecification { enum RequestBodies { - + public struct TextToSpeechInput: Codable { + + public let speaker: String + public let text: String + public let modelId: String + + public init( + speaker: String, + text: String, + modelId: String + ) { + self.speaker = speaker + self.text = text + self.modelId = modelId + } + } } } diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index f3371845..86a038d4 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -28,5 +28,23 @@ extension Rime.APISpecification { try container.encode(voices) } } + + public struct TextToSpeechOutput: Codable { + public let audioData: Data + + public init(audioData: Data) { + self.audioData = audioData + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.audioData = try container.decode(Data.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(audioData) + } + } } } diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift index c689a252..d041c381 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift @@ -62,6 +62,10 @@ extension Rime { @GET @Path("/data/voices/voice_details.json") var listVoices = Endpoint() + + @POST + @Path("/v1/rime-tts") + var textToSpeech = Endpoint() } } diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index 1ccc3f10..f579382c 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -60,4 +60,21 @@ extension Rime.Client { public func getAllAvailableVoices() async throws -> [Rime.Voice] { try await run(\.listVoices).voices } + + public func streamTextToSpeech( + text: String, + voice: String, + model: Rime.Model + ) async throws -> Data { + + let input = Rime.APISpecification.RequestBodies.TextToSpeechInput( + speaker: voice, + text: text, + modelId: model.rawValue + ) + + let responseData = try await run(\.textToSpeech, with: input) + + return responseData.audioData + } } From 39302c2e854674090193311a33146942bb56e122 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 21 Nov 2024 16:20:09 -0700 Subject: [PATCH 23/73] Text to Speech --- .../Rime.APISpecification.RequestBodies.swift | 6 ++++ ...Rime.APISpecification.ResponseBodies.swift | 32 +++++++++++++---- .../API/Rime.APISpecification.swift | 9 +++-- .../Rime/Intramodular/Models/Rime.Voice.swift | 36 +++++++------------ Sources/Rime/Intramodular/Rime.Client.swift | 2 +- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift index 2d39e1e7..f0348302 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift @@ -17,6 +17,12 @@ extension Rime.APISpecification { public let text: String public let modelId: String + enum CodingKeys: CodingKey { + case speaker + case text + case modelId + } + public init( speaker: String, text: String, diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index 86a038d4..1d053d23 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -30,20 +30,38 @@ extension Rime.APISpecification { } public struct TextToSpeechOutput: Codable { - public let audioData: Data + public let audioContent: Data - public init(audioData: Data) { - self.audioData = audioData + public init(audioContent: Data) { + self.audioContent = audioContent + } + + enum CodingKeys: String, CodingKey { + case audioContent } public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.audioData = try container.decode(Data.self) + let container = try decoder.container(keyedBy: CodingKeys.self) + + let base64String = try container.decode(String.self, forKey: .audioContent) + + guard let audioData = Data(base64Encoded: base64String) else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: [CodingKeys.audioContent], + debugDescription: "Invalid base64 encoded string" + ) + ) + } + + self.audioContent = audioData } public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(audioData) + var container = encoder.container(keyedBy: CodingKeys.self) + + let base64String = audioContent.base64EncodedString() + try container.encode(base64String, forKey: .audioContent) } } } diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift index d041c381..5bd75dc2 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift @@ -37,7 +37,7 @@ extension Rime { public var apiKey: String? public init( - host: URL = URL(string: "https://users.rime.ai/")!, + host: URL = URL(string: "https://users.rime.ai")!, apiKey: String? = nil ) { self.host = host @@ -65,6 +65,7 @@ extension Rime { @POST @Path("/v1/rime-tts") + @Body(json: \.input) var textToSpeech = Endpoint() } } @@ -80,9 +81,13 @@ extension Rime.APISpecification { context: context ) + guard let apiKey = context.root.configuration.apiKey, !apiKey.isEmpty else { + throw Rime.APIError.apiKeyMissing + } + request = request .header("Accept", "application/json") - .header("Authorization", "Bearer \(context.root.configuration.apiKey ?? "")") + .header(.authorization(.bearer, apiKey)) .header(.contentType(.json)) return request diff --git a/Sources/Rime/Intramodular/Models/Rime.Voice.swift b/Sources/Rime/Intramodular/Models/Rime.Voice.swift index 13ae4e43..9f6d7402 100644 --- a/Sources/Rime/Intramodular/Models/Rime.Voice.swift +++ b/Sources/Rime/Intramodular/Models/Rime.Voice.swift @@ -14,11 +14,11 @@ extension Rime { public let id: ID public let name: String - public let age: String - public let country: String - public let region: String - public let demographic: String - public let genre: [String] + public let age: String? + public let country: String? + public let region: String? + public let demographic: String? + public let genre: [String]? enum CodingKeys: CodingKey { case id @@ -32,25 +32,15 @@ extension Rime { public init(from decoder: any Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: Rime.Voice.CodingKeys.self) - self.id = try container.decode(Rime.Voice.ID.self, forKey: Rime.Voice.CodingKeys.id) self.name = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.name) - self.age = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.age) - self.country = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.country) - self.region = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.region) - self.demographic = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.demographic) - self.genre = try container.decode([String].self, forKey: Rime.Voice.CodingKeys.genre) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: Rime.Voice.CodingKeys.self) - try container.encode(self.id, forKey: Rime.Voice.CodingKeys.id) - try container.encode(self.name, forKey: Rime.Voice.CodingKeys.name) - try container.encode(self.age, forKey: Rime.Voice.CodingKeys.age) - try container.encode(self.country, forKey: Rime.Voice.CodingKeys.country) - try container.encode(self.region, forKey: Rime.Voice.CodingKeys.region) - try container.encode(self.demographic, forKey: Rime.Voice.CodingKeys.demographic) - try container.encode(self.genre, forKey: Rime.Voice.CodingKeys.genre) + + self.id = .init(rawValue: self.name) + + self.age = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.age) + self.country = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.country) + self.region = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.region) + self.demographic = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.demographic) + self.genre = try container.decodeIfPresent([String].self, forKey: Rime.Voice.CodingKeys.genre) } - } } diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index f579382c..5afd9057 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -75,6 +75,6 @@ extension Rime.Client { let responseData = try await run(\.textToSpeech, with: input) - return responseData.audioData + return responseData.audioContent } } From 626c646925dc6b3695f75ab6594641c279036ed0 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 22 Nov 2024 11:16:03 -0700 Subject: [PATCH 24/73] clean voice with url --- ...layHT.APISpecification.RequestBodies.swift | 33 +++++++++++++++++++ .../API/PlayHT.APISpecification.swift | 6 ++++ .../PlayHT/Intramodular/PlayHT.Client.swift | 17 ++++++++-- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index 8aa73eda..79ddf26e 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -116,5 +116,38 @@ extension PlayHT.APISpecification { return result } } + + public struct InstantCloneVoiceWithURLInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let url: String + public let voiceName: String + + public init( + url: String, + voiceName: String + ) { + self.url = url + self.voiceName = voiceName + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result: HTTPRequest.Multipart.Content = .init() + + result.append( + .string( + named: "sample_file_url", + value: url + ) + ) + + result.append( + .text( + named: "voice_name", + value: voiceName + ) + ) + + return result + } + } } } diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index f478bd72..525ef06d 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -83,6 +83,12 @@ extension PlayHT { @Body(multipart: .input) var instantCloneVoice = Endpoint() + // Clone Voice + @POST + @Path("/cloned-voices/instant") + @Body(multipart: .input) + var instantCloneVoiceWithURL = Endpoint() + // Delete cloned voice @DELETE @Path("/cloned-voices") diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 41a370bf..e5ad9453 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -59,14 +59,14 @@ extension PlayHT.Client: _MIService { extension PlayHT.Client { public func getAllAvailableVoices() async throws -> [PlayHT.Voice] { - async let htVoices = playHTAvailableVoices() + async let htVoices = availableVoices() async let clonedVoices = clonedVoices() let (available, cloned) = try await (htVoices, clonedVoices) return available + cloned } - public func playHTAvailableVoices() async throws -> [PlayHT.Voice] { + public func availableVoices() async throws -> [PlayHT.Voice] { try await run(\.listVoices).voices } @@ -129,6 +129,19 @@ extension PlayHT.Client { return .init(rawValue: response.id) } + public func instantCloneVoice( + url: String, + name: String + ) async throws -> PlayHT.Voice.ID { + let input = PlayHT.APISpecification.RequestBodies.InstantCloneVoiceWithURLInput( + url: url, + voiceName: name + ) + + let response = try await run(\.instantCloneVoiceWithURL, with: input) + return .init(rawValue: response.id) + } + public func deleteClonedVoice( voice: PlayHT.Voice.ID ) async throws { From 1b8454b801f2e98d24486cef83a8c125f49fe1d6 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 22 Nov 2024 13:41:56 -0700 Subject: [PATCH 25/73] update --- ...Rime.APISpecification.ResponseBodies.swift | 7 +- .../API/Rime.APISpecification.swift | 40 +++++++++++- .../Rime.Client.OutputAudioType.swift | 23 +++++++ Sources/Rime/Intramodular/Rime.Client.swift | 64 +++++++++++++++++-- 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 Sources/Rime/Intramodular/Rime.Client.OutputAudioType.swift diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index 1d053d23..4e3d9ace 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -11,7 +11,7 @@ import Merge extension Rime.APISpecification { enum ResponseBodies { - public struct Voices: Codable { + public struct VoiceDetails: Codable { public let voices: [Rime.Voice] public init(voices: [Rime.Voice]) { @@ -29,6 +29,11 @@ extension Rime.APISpecification { } } + struct Voices: Codable { + let v1: [String] + let mist: [String] + } + public struct TextToSpeechOutput: Codable { public let audioContent: Data diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift index 5bd75dc2..f376be10 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.swift @@ -61,12 +61,50 @@ extension Rime { @GET @Path("/data/voices/voice_details.json") + var listVoiceDetails = Endpoint() + + @GET + @Path("/data/voices/voices/all.json") var listVoices = Endpoint() + // Streaming Audio + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var streamTextToSpeechMP3 = Endpoint() + + // TODO: - Fix below + + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var streamTextToSpeechPCM = Endpoint() + + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var streamTextToSpeechMULAW = Endpoint() + + // Non-Streaming Audio + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var textToSpeechMP3 = Endpoint() + + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var textToSpeechWAV = Endpoint() + + @POST + @Path("/v1/rime-tts") + @Body(json: \.input) + var textToSpeechOGG = Endpoint() + @POST @Path("/v1/rime-tts") @Body(json: \.input) - var textToSpeech = Endpoint() + var textToSpeechMULAW = Endpoint() } } diff --git a/Sources/Rime/Intramodular/Rime.Client.OutputAudioType.swift b/Sources/Rime/Intramodular/Rime.Client.OutputAudioType.swift new file mode 100644 index 00000000..ca92c513 --- /dev/null +++ b/Sources/Rime/Intramodular/Rime.Client.OutputAudioType.swift @@ -0,0 +1,23 @@ +// +// Rime.OutputAudioType.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Swift + +extension Rime.Client { + public enum StreamOutputAudioType { + case MP3 + case PCM + case MULAW + } + + public enum OutputAudioType { + case MP3 + case WAV + case OGG + case MULAW + } +} diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index 5afd9057..cbb8d388 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -57,13 +57,25 @@ extension Rime.Client: _MIService { } extension Rime.Client { - public func getAllAvailableVoices() async throws -> [Rime.Voice] { - try await run(\.listVoices).voices + public func getAllAvailableVoiceDetails() async throws -> [Rime.Voice] { + try await run(\.listVoiceDetails).voices + } + + public func getAllAvailableVoices( + model: Rime.Model + ) async throws -> [String] { + switch model { + case .mist: + return try await run(\.listVoices).mist + case .v1: + return try await run(\.listVoices).v1 + } } public func streamTextToSpeech( text: String, voice: String, + outputAudio: StreamOutputAudioType, model: Rime.Model ) async throws -> Data { @@ -73,8 +85,52 @@ extension Rime.Client { modelId: model.rawValue ) - let responseData = try await run(\.textToSpeech, with: input) + switch outputAudio { + case .MP3: + let responseData = try await run(\.streamTextToSpeechMP3, with: input) + + return responseData.audioContent + case .PCM: + let responseData = try await run(\.streamTextToSpeechPCM, with: input) + + return responseData.audioContent + case .MULAW: + let responseData = try await run(\.streamTextToSpeechMULAW, with: input) + + return responseData.audioContent + } + } + + public func textToSpeech( + text: String, + voice: String, + outputAudio: OutputAudioType, + model: Rime.Model + ) async throws -> Data { + + let input = Rime.APISpecification.RequestBodies.TextToSpeechInput( + speaker: voice, + text: text, + modelId: model.rawValue + ) - return responseData.audioContent + switch outputAudio { + case .MP3: + let responseData = try await run(\.textToSpeechMP3, with: input) + + return responseData.audioContent + case .WAV: + let responseData = try await run(\.textToSpeechWAV, with: input) + + return responseData.audioContent + case .OGG: + let responseData = try await run(\.textToSpeechOGG, with: input) + + return responseData.audioContent + case .MULAW: + let responseData = try await run(\.textToSpeechMULAW, with: input) + + return responseData.audioContent + } } } From 67a5485a37b27ff1499729cbc71315de709fc3e9 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Sat, 23 Nov 2024 04:00:46 +0530 Subject: [PATCH 26/73] Update package --- .../Anthropic.Client+CoreMI.swift | 14 +-- .../Intramodular/Anthropic.Client.swift | 2 +- .../Cohere/Intramodular/Cohere.Client.swift | 16 +-- .../Foundation/CoreMI.Context.swift | 32 +++++ ...ing.swift => CoreMI.RequestHandling.swift} | 0 .../Intramodular/Foundation/MIContext.swift | 30 ----- .../CoreMI._ServiceAccountProtocol.swift | 35 ++++++ .../CoreMI._ServiceClientProtocol.swift | 29 +++++ .../CoreMI._ServiceCredentialProtocol.swift | 53 +++++++++ .../CoreMI._ServiceCredentialTypes.swift | 58 +++++++++ .../CoreMI._ServiceVendorIdentifier.swift | 52 +++++++++ .../Intramodular/Service/_MIService.swift | 16 --- .../Service/_MIServiceAccount.swift | 26 ----- .../Service/_MIServiceCredential.swift | 44 ------- .../Service/_MIServiceTypeIdentifier.swift | 43 ------- .../Intramodular/TTS/SpeechSynthesizers.swift | 25 ++++ Sources/CoreMI/module.swift | 4 + .../Intramodular/ElevenLabs.Voice.swift | 25 ++-- .../ElevenLabs.VoiceSettings.swift | 110 ++++++++---------- Sources/Groq/Intramodular/Groq.Client.swift | 16 +-- Sources/Jina/Intramodular/Jina.Client.swift | 16 +-- .../LLMs/AbstractLLM.Capability.swift | 5 +- .../AbstractLLM.ChatCompletionDecodable.swift | 2 +- .../LLMs/Chat/AbstractLLM.ChatPrompt.swift | 2 +- .../PromptLiteralConvertible.swift | 2 +- .../Text Embeddings/_RawTextEmbedding.swift | 2 +- .../Mistral/Intramodular/Mistral.Client.swift | 16 +-- Sources/Ollama/Intramodular/Ollama.swift | 12 +- .../Intramodular/OpenAI.Client+CoreMI.swift | 14 +-- .../OpenAI/Intramodular/OpenAI.Client.swift | 2 +- .../Intramodular/Perplexity.Client.swift | 16 +-- .../PlayHT/Intramodular/PlayHT.Client.swift | 16 +-- .../Rime.APISpecification.RequestBodies.swift | 43 +++---- ...Rime.APISpecification.ResponseBodies.swift | 27 ++--- .../Rime/Intramodular/Models/Rime.Voice.swift | 52 ++++----- Sources/Rime/Intramodular/Rime.Client.swift | 53 +++++---- .../Intramodular/TogetherAI.Client.swift | 16 +-- .../Intramodular/VoyageAI.Client.swift | 16 +-- 38 files changed, 535 insertions(+), 407 deletions(-) create mode 100644 Sources/CoreMI/Intramodular/Foundation/CoreMI.Context.swift rename Sources/CoreMI/Intramodular/Foundation/{_MIRequestHandling.swift => CoreMI.RequestHandling.swift} (100%) delete mode 100644 Sources/CoreMI/Intramodular/Foundation/MIContext.swift create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceAccountProtocol.swift create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialProtocol.swift create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialTypes.swift create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift delete mode 100644 Sources/CoreMI/Intramodular/Service/_MIService.swift delete mode 100644 Sources/CoreMI/Intramodular/Service/_MIServiceAccount.swift delete mode 100644 Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift delete mode 100644 Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift create mode 100644 Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift diff --git a/Sources/Anthropic/Intramodular/Anthropic.Client+CoreMI.swift b/Sources/Anthropic/Intramodular/Anthropic.Client+CoreMI.swift index a9b176ab..07eed3bd 100644 --- a/Sources/Anthropic/Intramodular/Anthropic.Client+CoreMI.swift +++ b/Sources/Anthropic/Intramodular/Anthropic.Client+CoreMI.swift @@ -8,17 +8,17 @@ import Swallow extension Anthropic.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Anthropic else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Anthropic else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/Anthropic/Intramodular/Anthropic.Client.swift b/Sources/Anthropic/Intramodular/Anthropic.Client.swift index 73b2a514..89345627 100644 --- a/Sources/Anthropic/Intramodular/Anthropic.Client.swift +++ b/Sources/Anthropic/Intramodular/Anthropic.Client.swift @@ -11,7 +11,7 @@ extension Anthropic { @RuntimeDiscoverable public final class Client: HTTPClient, PersistentlyRepresentableType, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Anthropic + CoreMI._ServiceVendorIdentifier._Anthropic } public let interface: API diff --git a/Sources/Cohere/Intramodular/Cohere.Client.swift b/Sources/Cohere/Intramodular/Cohere.Client.swift index aa6e9d98..06d0f5c8 100644 --- a/Sources/Cohere/Intramodular/Cohere.Client.swift +++ b/Sources/Cohere/Intramodular/Cohere.Client.swift @@ -12,7 +12,7 @@ extension Cohere { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Cohere + CoreMI._ServiceVendorIdentifier._Cohere } public let interface: APISpecification @@ -34,17 +34,17 @@ extension Cohere { extension Cohere.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Cohere else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Cohere else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/CoreMI/Intramodular/Foundation/CoreMI.Context.swift b/Sources/CoreMI/Intramodular/Foundation/CoreMI.Context.swift new file mode 100644 index 00000000..60058ae8 --- /dev/null +++ b/Sources/CoreMI/Intramodular/Foundation/CoreMI.Context.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) Vatsal Manot +// + +import Combine +import Swallow +import SwiftDI + +extension CoreMI { + /// A context for machine intelligence. + public final class Context: ObservableObject { + @Published public var handlers: [any CoreMI.RequestHandling] = [] + + public func add(_ x: T) { + handlers.append(x) + } + + public func _firstHandler(ofType type: T.Type) async throws -> T { + try handlers.first(byUnwrapping: { try? cast($0, to: type) }).unwrap() + } + } +} + +public enum CoreMI { + public protocol Request { + + } + + public protocol RequestResult { + + } +} diff --git a/Sources/CoreMI/Intramodular/Foundation/_MIRequestHandling.swift b/Sources/CoreMI/Intramodular/Foundation/CoreMI.RequestHandling.swift similarity index 100% rename from Sources/CoreMI/Intramodular/Foundation/_MIRequestHandling.swift rename to Sources/CoreMI/Intramodular/Foundation/CoreMI.RequestHandling.swift diff --git a/Sources/CoreMI/Intramodular/Foundation/MIContext.swift b/Sources/CoreMI/Intramodular/Foundation/MIContext.swift deleted file mode 100644 index a3137b17..00000000 --- a/Sources/CoreMI/Intramodular/Foundation/MIContext.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import Combine -import Swallow -import SwiftDI - -/// A context for machine intelligence. -public final class MIContext: ObservableObject { - @Published public var handlers: [any CoreMI.RequestHandling] = [] - - public func add(_ x: T) { - handlers.append(x) - } - - public func _firstHandler(ofType type: T.Type) async throws -> T { - try handlers.first(byUnwrapping: { try? cast($0, to: type) }).unwrap() - } -} - -public enum CoreMI { - public protocol Request { - - } - - public protocol RequestResult { - - } -} diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceAccountProtocol.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceAccountProtocol.swift new file mode 100644 index 00000000..5d8280e8 --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceAccountProtocol.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swallow + +extension CoreMI { + /// An account used to authenticate access to a service. + public protocol _ServiceAccountProtocol: Hashable { + var serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier? { get throws } + var credential: (any CoreMI._ServiceCredentialProtocol)? { get throws } + } + + public struct _AnyServiceAccount: _MIServiceAccount { + public let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier? + @_HashableExistential + public var credential: (any CoreMI._ServiceCredentialProtocol)? + + public init( + serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier?, + credential: (any CoreMI._ServiceCredentialProtocol)? + ) { + self.serviceVendorIdentifier = serviceVendorIdentifier + self.credential = credential + } + } +} + +// MARK: - Deprecated + +@available(*, deprecated) +public typealias _MIServiceAccount = CoreMI._ServiceAccountProtocol +@available(*, deprecated) +public typealias _AnyMIServiceAccount = CoreMI._AnyServiceAccount diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift new file mode 100644 index 00000000..9fd23037 --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swallow + +extension CoreMI { + /// A client for an AI/ML service. + public protocol _ServiceClientProtocol: PersistentlyRepresentableType { + init(account: (any CoreMI._ServiceAccountProtocol)?) async throws + } + + public enum _ServiceClientError: Error { + case incompatibleVendor(CoreMI._ServiceVendorIdentifier) + case invalidCredential((any CoreMI._ServiceCredentialProtocol)?) + + public static var invalidCredential: Self { + Self.invalidCredential(nil) + } + } +} + +// MARK: - Deprecated + +@available(*, deprecated) +public typealias _MIService = CoreMI._ServiceClientProtocol +@available(*, deprecated) +public typealias _MIServiceError = CoreMI._ServiceClientError diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialProtocol.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialProtocol.swift new file mode 100644 index 00000000..399e8c21 --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialProtocol.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swift + +extension CoreMI { + public protocol _ServiceCredentialProtocol: Codable, Hashable, Sendable { + var credentialType: CoreMI._ServiceCredentialTypeIdentifier { get } + } +} + +// MARK: - Conformees + +extension CoreMI._ServiceCredentialTypes { + public struct APIKeyCredential: CoreMI._ServiceCredentialProtocol { + public var credentialType: CoreMI._ServiceCredentialTypeIdentifier { + CoreMI._ServiceCredentialTypeIdentifier.apiKey + } + + public let apiKey: String + + public init(apiKey: String) { + self.apiKey = apiKey + } + } +} + +extension CoreMI._ServiceCredentialTypes { + @HadeanIdentifier("jikok-fafan-nadij-javub") + public struct PlayHTCredential: CoreMI._ServiceCredentialProtocol { + public var credentialType: CoreMI._ServiceCredentialTypeIdentifier { + .custom(Self.self) + } + + public let userID: String + public let apiKey: String + + public init( + userID: String, + apiKey: String + ) { + self.userID = userID + self.apiKey = apiKey + } + } +} + +// MARK: - Deprecated + +@available(*, deprecated, renamed: "CoreMI._ServiceCredentialTypes.APIKeyCredential") +public typealias _MIServiceAPIKeyCredential = CoreMI._ServiceCredentialTypes.APIKeyCredential diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialTypes.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialTypes.swift new file mode 100644 index 00000000..dbe90c8b --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceCredentialTypes.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swift + +extension CoreMI { + public enum _ServiceCredentialTypes { + + } +} + +extension CoreMI { + public enum _ServiceCredentialTypeIdentifier: Codable, Hashable, RawRepresentable, Sendable { + case apiKey + case custom(HadeanIdentifier) + + public var rawValue: String { + switch self { + case .apiKey: + return "apiKey" + case .custom(let id): + return id.description + } + } + + public init?(rawValue: String) { + switch rawValue { + case CoreMI._ServiceCredentialTypeIdentifier.apiKey.rawValue: + self = .apiKey + default: + if let identifier = HadeanIdentifier(rawValue) { + self = .custom(identifier) + } else { + return nil + } + } + } + + public static func custom(_ type: T.Type) -> Self { + return .custom(type.hadeanIdentifier) + } + + public func encode(to encoder: any Encoder) throws { + try rawValue.encode(to: encoder) + } + + public init(from decoder: any Decoder) throws { + self = try Self(rawValue: String(from: decoder)).unwrap() + } + } +} + +// MARK: - Deprecated + +@available(*, deprecated, renamed: "CoreMI._ServiceCredentialTypeIdentifier") +public typealias _MIServiceCredentialType = CoreMI._ServiceCredentialTypeIdentifier diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift new file mode 100644 index 00000000..02d956ba --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swift + +extension CoreMI { + public struct _ServiceVendorIdentifier: Codable, Hashable, RawRepresentable, Sendable { + public let rawValue: HadeanIdentifier + + public init(rawValue: HadeanIdentifier) { + self.rawValue = rawValue + } + } + + public protocol _ServiceVendorIdentifierConvertible { + func __conversion() throws -> CoreMI._ServiceVendorIdentifier + } +} + +extension CoreMI._ServiceVendorIdentifier: PersistentIdentifier { + public var body: some IdentityRepresentation { + rawValue + } +} + +extension CoreMI._ServiceVendorIdentifier { + public static let _Anthropic = CoreMI._ServiceVendorIdentifier(rawValue: "puhif-pudav-gujir-nubup") + public static let _Cohere = CoreMI._ServiceVendorIdentifier(rawValue: "guzob-fipin-navij-duvon") + public static let _ElevenLabs = CoreMI._ServiceVendorIdentifier(rawValue: "jatap-jogaz-ritiz-vibok") + public static let _Fal = CoreMI._ServiceVendorIdentifier(rawValue: "povar-firul-milij-jopat") + public static let _Groq = CoreMI._ServiceVendorIdentifier(rawValue: "jabub-potuv-juniv-nodik") + public static let _HuggingFace = CoreMI._ServiceVendorIdentifier(rawValue: "jutot-gugal-luzoh-vorig") + public static let _Mistral = CoreMI._ServiceVendorIdentifier(rawValue: "vogas-fohig-mokij-titun") + public static let _Ollama = CoreMI._ServiceVendorIdentifier(rawValue: "sotap-boris-navam-mitoh") + public static let _OpenAI = CoreMI._ServiceVendorIdentifier(rawValue: "vodih-vakam-hiduz-tosob") + public static let _Perplexity = CoreMI._ServiceVendorIdentifier(rawValue: "dohug-muboz-bopuz-kasar") + public static let _PlayHT = CoreMI._ServiceVendorIdentifier(rawValue: "foluv-jufuk-zuhok-hofid") + public static let _Replicate = CoreMI._ServiceVendorIdentifier(rawValue: "dovon-vatig-posov-luvis") + public static let _Rime = CoreMI._ServiceVendorIdentifier(rawValue: "tohaz-zivir-bosov-minog") + public static let _Jina = CoreMI._ServiceVendorIdentifier(rawValue: "bozud-sipup-natin-bizif") + public static let _TogetherAI = CoreMI._ServiceVendorIdentifier(rawValue: "pafob-vopoj-lurig-zilur") + public static let _VoyageAI = CoreMI._ServiceVendorIdentifier(rawValue: "hajat-fufoh-janaf-disam") +} + +// MARK: - Deprecated + +@available(*, deprecated) +public typealias _MIServiceTypeIdentifier = CoreMI._ServiceVendorIdentifier +@available(*, deprecated) +public typealias _MIServiceTypeIdentifierConvertible = CoreMI._ServiceVendorIdentifierConvertible diff --git a/Sources/CoreMI/Intramodular/Service/_MIService.swift b/Sources/CoreMI/Intramodular/Service/_MIService.swift deleted file mode 100644 index 7dcf937d..00000000 --- a/Sources/CoreMI/Intramodular/Service/_MIService.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import Swallow - -/// A machine intelligence service. -public protocol _MIService: PersistentlyRepresentableType { - init(account: (any _MIServiceAccount)?) async throws -} - -public enum _MIServiceError: Error { - case serviceTypeIncompatible(_MIServiceTypeIdentifier) - case invalidCredentials((any _MIServiceCredential)?) -} diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceAccount.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceAccount.swift deleted file mode 100644 index 0a32b59c..00000000 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceAccount.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import Swallow - -/// An account used to authenticate access to a service. -public protocol _MIServiceAccount: Hashable { - var serviceIdentifier: _MIServiceTypeIdentifier { get } - var credential: (any _MIServiceCredential)? { get } -} - -public struct _AnyMIServiceAccount: _MIServiceAccount { - public let serviceIdentifier: _MIServiceTypeIdentifier - @_HashableExistential - public var credential: (any _MIServiceCredential)? - - public init( - serviceIdentifier: _MIServiceTypeIdentifier, - credential: (any _MIServiceCredential)? - ) { - self.serviceIdentifier = serviceIdentifier - self.credential = credential - } -} diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift deleted file mode 100644 index 8873349c..00000000 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceCredential.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import Swift - -public enum _MIServiceCredentialType: String, PersistentIdentifier { - case apiKey = "apiKey" - case userIDAndAPIKey = "userIDAndAPIKey" -} - -public protocol _MIServiceCredential: Codable, Hashable, Sendable { - var credentialType: _MIServiceCredentialType { get } -} - -public struct _MIServiceAPIKeyCredential: _MIServiceCredential { - public var credentialType: _MIServiceCredentialType { - _MIServiceCredentialType.apiKey - } - - public let apiKey: String - - public init(apiKey: String) { - self.apiKey = apiKey - } -} - -public struct _MIServiceUserIDAndAPIKeyCredential: _MIServiceCredential { - public var credentialType: _MIServiceCredentialType { - _MIServiceCredentialType.userIDAndAPIKey - } - - public let userID: String - public let apiKey: String - - public init( - userID: String, - apiKey: String - ) { - self.userID = userID - self.apiKey = apiKey - } -} diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift deleted file mode 100644 index babec233..00000000 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import Swift - -public struct _MIServiceTypeIdentifier: Codable, Hashable, RawRepresentable, Sendable { - public let rawValue: HadeanIdentifier - - public init(rawValue: HadeanIdentifier) { - self.rawValue = rawValue - } -} - -public protocol _MIServiceTypeIdentifierConvertible { - func __conversion() throws -> _MIServiceTypeIdentifier -} - -extension _MIServiceTypeIdentifier: PersistentIdentifier { - public var body: some IdentityRepresentation { - rawValue - } -} - -extension _MIServiceTypeIdentifier { - public static let _Anthropic = _MIServiceTypeIdentifier(rawValue: "puhif-pudav-gujir-nubup") - public static let _Fal = _MIServiceTypeIdentifier(rawValue: "povar-firul-milij-jopat") - public static let _HuggingFace = _MIServiceTypeIdentifier(rawValue: "jutot-gugal-luzoh-vorig") - public static let _Mistral = _MIServiceTypeIdentifier(rawValue: "vogas-fohig-mokij-titun") - public static let _Groq = _MIServiceTypeIdentifier(rawValue: "jabub-potuv-juniv-nodik") - public static let _Ollama = _MIServiceTypeIdentifier(rawValue: "sotap-boris-navam-mitoh") - public static let _OpenAI = _MIServiceTypeIdentifier(rawValue: "vodih-vakam-hiduz-tosob") - public static let _Perplexity = _MIServiceTypeIdentifier(rawValue: "dohug-muboz-bopuz-kasar") - public static let _Replicate = _MIServiceTypeIdentifier(rawValue: "dovon-vatig-posov-luvis") - public static let _ElevenLabs = _MIServiceTypeIdentifier(rawValue: "jatap-jogaz-ritiz-vibok") - public static let _Jina = _MIServiceTypeIdentifier(rawValue: "bozud-sipup-natin-bizif") - public static let _VoyageAI = _MIServiceTypeIdentifier(rawValue: "hajat-fufoh-janaf-disam") - public static let _Cohere = _MIServiceTypeIdentifier(rawValue: "guzob-fipin-navij-duvon") - public static let _TogetherAI = _MIServiceTypeIdentifier(rawValue: "pafob-vopoj-lurig-zilur") - public static let _PlayHT = _MIServiceTypeIdentifier(rawValue: "foluv-jufuk-zuhok-hofid") - public static let _Rime = _MIServiceTypeIdentifier(rawValue: "tohaz-zivir-bosov-minog") -} diff --git a/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift new file mode 100644 index 00000000..1a9f944f --- /dev/null +++ b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swift + +public enum SpeechSynthesizers { + +} + +extension SpeechSynthesizers { + public protocol VoiceName: Codable, Hashable, Identifiable { + + } +} + +extension SpeechSynthesizers { + public struct AnyVoiceName: SpeechSynthesizers.VoiceName { + public var id: AnyPersistentIdentifier + public var displayName: String + public var userInfo: UserInfo + } +} + diff --git a/Sources/CoreMI/module.swift b/Sources/CoreMI/module.swift index 58f26b20..a52c526d 100644 --- a/Sources/CoreMI/module.swift +++ b/Sources/CoreMI/module.swift @@ -11,9 +11,13 @@ public enum _module { // MARK: - Deprecated +@available(*, deprecated, renamed: "CoreMI.Context") +public typealias MIContext = CoreMI.Context @available(*, deprecated, renamed: "ModelIdentifier") public typealias _MLModelIdentifier = ModelIdentifier @available(*, deprecated, renamed: "ModelIdentifierConvertible") public typealias _MLModelIdentifierConvertible = ModelIdentifierConvertible @available(*, deprecated, renamed: "ModelIdentifier.Provider") public typealias MLModelProvider = ModelIdentifier.Provider +@available(*, deprecated, renamed: "CoreMI._ServiceCredentialProtocol") +public typealias _MIServiceCredential = CoreMI._ServiceCredentialProtocol diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift index 6c9b83fc..3a54532f 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift @@ -6,14 +6,13 @@ import Foundation import Swift extension ElevenLabs { - // MARK: - Voice Model - public struct Voice: Codable, Hashable, Identifiable, Sendable { + public struct Voice: Hashable, Identifiable, Sendable { public typealias ID = _TypeAssociatedID public let voiceID: String public let name: String public let description: String? - public let isOwner: Bool + public let isOwner: Bool? public var id: ID { ID(rawValue: voiceID) @@ -23,19 +22,23 @@ extension ElevenLabs { voiceID: String, name: String, description: String?, - isOwner: Bool + isOwner: Bool? ) { self.voiceID = voiceID self.name = name self.description = description self.isOwner = isOwner } - - enum CodingKeys: String, CodingKey { - case voiceID = "voiceId" - case name - case description - case isOwner - } + } +} + +// MARK: - Conformances + +extension ElevenLabs.Voice: Codable { + enum CodingKeys: String, CodingKey { + case voiceID = "voiceId" + case name + case description + case isOwner } } diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift index 1c5bd481..1ffb7947 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift @@ -6,7 +6,6 @@ import Foundation extension ElevenLabs { public struct VoiceSettings: Codable, Sendable, Hashable { - public enum Setting: String, Codable, Sendable { case stability case similarityBoost = "similarity_boost" @@ -30,70 +29,20 @@ extension ElevenLabs { public var removeBackgroundNoise: Bool - public init(stability: Double, - similarityBoost: Double, - styleExaggeration: Double, - speakerBoost: Bool, - removeBackgroundNoise: Bool) { - self.stability = max(0, min(1, stability)) - self.similarityBoost = max(0, min(1, similarityBoost)) - self.styleExaggeration = max(0, min(1, styleExaggeration)) - self.speakerBoost = speakerBoost - self.removeBackgroundNoise = removeBackgroundNoise - } - - public init(stability: Double? = nil, - similarityBoost: Double? = nil, - styleExaggeration: Double? = nil, - speakerBoost: Bool? = nil, - removeBackgroundNoise: Bool? = nil) { + public init( + stability: Double? = nil, + similarityBoost: Double? = nil, + styleExaggeration: Double? = nil, + speakerBoost: Bool? = nil, + removeBackgroundNoise: Bool? = nil + ) { self.stability = stability.map { max(0, min(1, $0)) } ?? 0.5 self.similarityBoost = similarityBoost.map { max(0, min(1, $0)) } ?? 0.75 self.styleExaggeration = styleExaggeration.map { max(0, min(1, $0)) } ?? 0 self.speakerBoost = speakerBoost ?? true self.removeBackgroundNoise = removeBackgroundNoise ?? false } - - public init(stability: Double) { - self.init( - stability: stability, - similarityBoost: 0.75, - styleExaggeration: 0, - speakerBoost: true, - removeBackgroundNoise: false - ) - } - - public init(similarityBoost: Double) { - self.init( - stability: 0.5, - similarityBoost: similarityBoost, - styleExaggeration: 0, - speakerBoost: true, - removeBackgroundNoise: false - ) - } - - public init(styleExaggeration: Double) { - self.init( - stability: 0.5, - similarityBoost: 0.75, - styleExaggeration: styleExaggeration, - speakerBoost: true, - removeBackgroundNoise: false - ) - } - - public init(speakerBoost: Bool) { - self.init( - stability: 0.5, - similarityBoost: 0.75, - styleExaggeration: 0, - speakerBoost: speakerBoost, - removeBackgroundNoise: false - ) - } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -106,3 +55,46 @@ extension ElevenLabs { } } +// MARK: - Initializers + +extension ElevenLabs.VoiceSettings { + public init(stability: Double) { + self.init( + stability: stability, + similarityBoost: nil, + styleExaggeration: nil, + speakerBoost: nil, + removeBackgroundNoise: nil + ) + } + + public init(similarityBoost: Double) { + self.init( + stability: nil, + similarityBoost: similarityBoost, + styleExaggeration: nil, + speakerBoost: nil, + removeBackgroundNoise: nil + ) + } + + public init(styleExaggeration: Double) { + self.init( + stability: nil, + similarityBoost: nil, + styleExaggeration: styleExaggeration, + speakerBoost: true, + removeBackgroundNoise: nil + ) + } + + public init(speakerBoost: Bool) { + self.init( + stability: nil, + similarityBoost: nil, + styleExaggeration: nil, + speakerBoost: speakerBoost, + removeBackgroundNoise: nil + ) + } +} diff --git a/Sources/Groq/Intramodular/Groq.Client.swift b/Sources/Groq/Intramodular/Groq.Client.swift index 4ad01e41..8679de76 100644 --- a/Sources/Groq/Intramodular/Groq.Client.swift +++ b/Sources/Groq/Intramodular/Groq.Client.swift @@ -12,7 +12,7 @@ extension Groq { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Groq + CoreMI._ServiceVendorIdentifier._Groq } public let interface: APISpecification @@ -34,17 +34,17 @@ extension Groq { extension Groq.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Groq else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Groq else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/Jina/Intramodular/Jina.Client.swift b/Sources/Jina/Intramodular/Jina.Client.swift index 6f50b710..c52d9d80 100644 --- a/Sources/Jina/Intramodular/Jina.Client.swift +++ b/Sources/Jina/Intramodular/Jina.Client.swift @@ -12,7 +12,7 @@ extension Jina { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Jina + CoreMI._ServiceVendorIdentifier._Jina } public let interface: APISpecification @@ -34,17 +34,17 @@ extension Jina { extension Jina.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Jina else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Jina else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.Capability.swift b/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.Capability.swift index 736c3e06..3ccc3696 100644 --- a/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.Capability.swift +++ b/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.Capability.swift @@ -5,5 +5,8 @@ import SwiftUIX extension AbstractLLM { - + public enum Capability { + case functionCalling + case vision + } } diff --git a/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.ChatCompletionDecodable.swift b/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.ChatCompletionDecodable.swift index ac45184e..ca505c17 100644 --- a/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.ChatCompletionDecodable.swift +++ b/Sources/LargeLanguageModels/Intramodular/LLMs/AbstractLLM.ChatCompletionDecodable.swift @@ -45,7 +45,7 @@ extension AbstractLLM.ChatCompletion { } } -// MARK: - Implemented Conformances +// MARK: - Conformees extension AbstractLLM.ChatFunctionCall: AbstractLLM.ChatCompletionDecodable { public static func decode( diff --git a/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift b/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift index 54e31443..60654d65 100644 --- a/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift +++ b/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift @@ -1,5 +1,5 @@ // -// Copyright (c) Vatsal Manot +// Copyright (c) Vatsal Manot // import CorePersistence diff --git a/Sources/LargeLanguageModels/Intramodular/Prompt Literal/PromptLiteralConvertible.swift b/Sources/LargeLanguageModels/Intramodular/Prompt Literal/PromptLiteralConvertible.swift index c47f626e..2a8d65cb 100644 --- a/Sources/LargeLanguageModels/Intramodular/Prompt Literal/PromptLiteralConvertible.swift +++ b/Sources/LargeLanguageModels/Intramodular/Prompt Literal/PromptLiteralConvertible.swift @@ -20,7 +20,7 @@ extension PromptLiteralConvertible { } } -// MARK: - Implemented Conformances +// MARK: - Conformees extension String: _PromptLiteralConvertible { public func _toPromptLiteral() throws -> PromptLiteral { diff --git a/Sources/LargeLanguageModels/Intramodular/Text Embeddings/_RawTextEmbedding.swift b/Sources/LargeLanguageModels/Intramodular/Text Embeddings/_RawTextEmbedding.swift index 910c7719..49917001 100644 --- a/Sources/LargeLanguageModels/Intramodular/Text Embeddings/_RawTextEmbedding.swift +++ b/Sources/LargeLanguageModels/Intramodular/Text Embeddings/_RawTextEmbedding.swift @@ -21,7 +21,7 @@ public struct _RawTextEmbedding: Hashable, Sendable { } } -// MARK: - Implemented Conformances +// MARK: - Conformees extension _RawTextEmbedding: Codable { @inlinable diff --git a/Sources/Mistral/Intramodular/Mistral.Client.swift b/Sources/Mistral/Intramodular/Mistral.Client.swift index 563baca7..3647e1b3 100644 --- a/Sources/Mistral/Intramodular/Mistral.Client.swift +++ b/Sources/Mistral/Intramodular/Mistral.Client.swift @@ -12,7 +12,7 @@ extension Mistral { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Mistral + CoreMI._ServiceVendorIdentifier._Mistral } public let interface: APISpecification @@ -34,17 +34,17 @@ extension Mistral { extension Mistral.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Mistral else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Mistral else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/Ollama/Intramodular/Ollama.swift b/Sources/Ollama/Intramodular/Ollama.swift index 2b3046c3..77e3ad27 100644 --- a/Sources/Ollama/Intramodular/Ollama.swift +++ b/Sources/Ollama/Intramodular/Ollama.swift @@ -78,7 +78,7 @@ extension Ollama { extension Ollama: PersistentlyRepresentableType { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Ollama + CoreMI._ServiceVendorIdentifier._Ollama } } @@ -88,13 +88,13 @@ extension Ollama: _MIService { extension _MIService where Self == Ollama { public init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Ollama else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Ollama else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } self = .shared diff --git a/Sources/OpenAI/Intramodular/OpenAI.Client+CoreMI.swift b/Sources/OpenAI/Intramodular/OpenAI.Client+CoreMI.swift index 778281ec..32fceea4 100644 --- a/Sources/OpenAI/Intramodular/OpenAI.Client+CoreMI.swift +++ b/Sources/OpenAI/Intramodular/OpenAI.Client+CoreMI.swift @@ -7,17 +7,17 @@ import CorePersistence extension OpenAI.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._OpenAI else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._OpenAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/OpenAI/Intramodular/OpenAI.Client.swift b/Sources/OpenAI/Intramodular/OpenAI.Client.swift index 581022f7..a128059c 100644 --- a/Sources/OpenAI/Intramodular/OpenAI.Client.swift +++ b/Sources/OpenAI/Intramodular/OpenAI.Client.swift @@ -206,7 +206,7 @@ extension OpenAI.Client: _MaybeAsyncProtocol { extension OpenAI.Client: PersistentlyRepresentableType { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._OpenAI + CoreMI._ServiceVendorIdentifier._OpenAI } } diff --git a/Sources/Perplexity/Intramodular/Perplexity.Client.swift b/Sources/Perplexity/Intramodular/Perplexity.Client.swift index 8816d702..aac5a6a5 100644 --- a/Sources/Perplexity/Intramodular/Perplexity.Client.swift +++ b/Sources/Perplexity/Intramodular/Perplexity.Client.swift @@ -12,7 +12,7 @@ extension Perplexity { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Perplexity + CoreMI._ServiceVendorIdentifier._Perplexity } public let interface: APISpecification @@ -34,17 +34,17 @@ extension Perplexity { extension Perplexity.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._Perplexity else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._Perplexity else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index e5ad9453..22fed003 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -15,7 +15,7 @@ extension PlayHT { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._PlayHT + CoreMI._ServiceVendorIdentifier._PlayHT } public typealias API = PlayHT.APISpecification @@ -39,17 +39,17 @@ extension PlayHT { extension PlayHT.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._PlayHT else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._PlayHT else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceUserIDAndAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.PlayHTCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey, userID: credential.userID) diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift index f0348302..6aa5a1c6 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.RequestBodies.swift @@ -11,27 +11,30 @@ import Merge extension Rime.APISpecification { enum RequestBodies { - public struct TextToSpeechInput: Codable { + + } +} - public let speaker: String - public let text: String - public let modelId: String - - enum CodingKeys: CodingKey { - case speaker - case text - case modelId - } - - public init( - speaker: String, - text: String, - modelId: String - ) { - self.speaker = speaker - self.text = text - self.modelId = modelId - } +extension Rime.APISpecification.RequestBodies { + struct TextToSpeechInput: Codable { + fileprivate enum CodingKeys: String, CodingKey { + case speaker + case text + case modelID = "modelId" + } + + let speaker: String + let text: String + let modelID: String + + init( + speaker: String, + text: String, + modelID: String + ) { + self.speaker = speaker + self.text = text + self.modelID = modelID } } } diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index 1d053d23..a1889a49 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -14,7 +14,9 @@ extension Rime.APISpecification { public struct Voices: Codable { public let voices: [Rime.Voice] - public init(voices: [Rime.Voice]) { + public init( + voices: [Rime.Voice] + ) { self.voices = voices } @@ -30,37 +32,28 @@ extension Rime.APISpecification { } public struct TextToSpeechOutput: Codable { + fileprivate enum CodingKeys: String, CodingKey { + case audioContent + } + public let audioContent: Data public init(audioContent: Data) { self.audioContent = audioContent } - - enum CodingKeys: String, CodingKey { - case audioContent - } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let base64String = try container.decode(String.self, forKey: .audioContent) - guard let audioData = Data(base64Encoded: base64String) else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: [CodingKeys.audioContent], - debugDescription: "Invalid base64 encoded string" - ) - ) - } - - self.audioContent = audioData + self.audioContent = try Data(base64Encoded: base64String).unwrap() } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) let base64String = audioContent.base64EncodedString() + try container.encode(base64String, forKey: .audioContent) } } diff --git a/Sources/Rime/Intramodular/Models/Rime.Voice.swift b/Sources/Rime/Intramodular/Models/Rime.Voice.swift index 9f6d7402..1a341b41 100644 --- a/Sources/Rime/Intramodular/Models/Rime.Voice.swift +++ b/Sources/Rime/Intramodular/Models/Rime.Voice.swift @@ -9,38 +9,38 @@ import Foundation import Swallow extension Rime { - public struct Voice: Codable, Hashable, Identifiable { - public typealias ID = _TypeAssociatedID - - public let id: ID + public struct Voice: Hashable { public let name: String public let age: String? public let country: String? public let region: String? public let demographic: String? public let genre: [String]? - - enum CodingKeys: CodingKey { - case id - case name - case age - case country - case region - case demographic - case genre - } - - public init(from decoder: any Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: Rime.Voice.CodingKeys.self) - self.name = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.name) - - self.id = .init(rawValue: self.name) + } +} + +// MARK: - Conformances - self.age = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.age) - self.country = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.country) - self.region = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.region) - self.demographic = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.demographic) - self.genre = try container.decodeIfPresent([String].self, forKey: Rime.Voice.CodingKeys.genre) - } +extension Rime.Voice: Codable { + enum CodingKeys: CodingKey { + case name + case age + case country + case region + case demographic + case genre + } + + public init( + from decoder: any Decoder + ) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: Rime.Voice.CodingKeys.self) + + self.name = try container.decode(String.self, forKey: Rime.Voice.CodingKeys.name) + self.age = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.age) + self.country = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.country) + self.region = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.region) + self.demographic = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.demographic) + self.genre = try container.decodeIfPresent([String].self, forKey: Rime.Voice.CodingKeys.genre) } } diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index 5afd9057..15994d26 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -15,7 +15,7 @@ extension Rime { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._Rime + CoreMI._ServiceVendorIdentifier._Rime } public typealias API = Rime.APISpecification @@ -25,37 +25,22 @@ extension Rime { public let session: Session public var sessionCache: EmptyKeyedCache - public required init(configuration: API.Configuration) { + public required init( + configuration: API.Configuration + ) { self.interface = API(configuration: configuration) self.session = HTTPSession.shared self.sessionCache = .init() } - public convenience init(apiKey: String) { + public convenience init( + apiKey: String + ) { self.init(configuration: .init(apiKey: apiKey)) } } } -extension Rime.Client: _MIService { - public convenience init( - account: (any _MIServiceAccount)? - ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier - - guard serviceIdentifier == _MIServiceTypeIdentifier._PlayHT else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) - } - - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) - } - - self.init(apiKey: credential.apiKey) - } -} - extension Rime.Client { public func getAllAvailableVoices() async throws -> [Rime.Voice] { try await run(\.listVoices).voices @@ -66,11 +51,10 @@ extension Rime.Client { voice: String, model: Rime.Model ) async throws -> Data { - let input = Rime.APISpecification.RequestBodies.TextToSpeechInput( speaker: voice, text: text, - modelId: model.rawValue + modelID: model.rawValue ) let responseData = try await run(\.textToSpeech, with: input) @@ -78,3 +62,24 @@ extension Rime.Client { return responseData.audioContent } } + +// MARK: - Conformances + +extension Rime.Client: _MIService { + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._PlayHT else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift index 972f62a9..1a0e8b5f 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift @@ -12,7 +12,7 @@ extension TogetherAI { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._TogetherAI + CoreMI._ServiceVendorIdentifier._TogetherAI } public let interface: APISpecification @@ -34,17 +34,17 @@ extension TogetherAI { extension TogetherAI.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._TogetherAI else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._TogetherAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) diff --git a/Sources/VoyageAI/Intramodular/VoyageAI.Client.swift b/Sources/VoyageAI/Intramodular/VoyageAI.Client.swift index b77111a1..e0ffaf1e 100644 --- a/Sources/VoyageAI/Intramodular/VoyageAI.Client.swift +++ b/Sources/VoyageAI/Intramodular/VoyageAI.Client.swift @@ -12,7 +12,7 @@ extension VoyageAI { @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public static var persistentTypeRepresentation: some IdentityRepresentation { - _MIServiceTypeIdentifier._VoyageAI + CoreMI._ServiceVendorIdentifier._VoyageAI } public let interface: APISpecification @@ -34,17 +34,17 @@ extension VoyageAI { extension VoyageAI.Client: _MIService { public convenience init( - account: (any _MIServiceAccount)? + account: (any CoreMI._ServiceAccountProtocol)? ) async throws { - let account: any _MIServiceAccount = try account.unwrap() - let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - guard serviceIdentifier == _MIServiceTypeIdentifier._VoyageAI else { - throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._VoyageAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) } - guard let credential = account.credential as? _MIServiceAPIKeyCredential else { - throw _MIServiceError.invalidCredentials(account.credential) + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) } self.init(apiKey: credential.apiKey) From cf8952a265c58acca9d8f11502c3a072bcd28c69 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Sat, 23 Nov 2024 04:07:20 +0530 Subject: [PATCH 27/73] Update package --- .../TTS/SpeechSynthesizers.VoiceName.swift | 28 +++++++++++++++++++ .../Intramodular/TTS/SpeechSynthesizers.swift | 15 ---------- 2 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.VoiceName.swift diff --git a/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.VoiceName.swift b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.VoiceName.swift new file mode 100644 index 00000000..a33239d2 --- /dev/null +++ b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.VoiceName.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import Swift + +extension SpeechSynthesizers { + public protocol VoiceName: Codable, Hashable, Identifiable { + + } +} + +extension SpeechSynthesizers { + public struct AnyVoiceName: SpeechSynthesizers.VoiceName { + public var id: AnyPersistentIdentifier + public var displayName: String + public var userInfo: UserInfo + } + + public protocol _AnyVoiceNameInitiable { + init(voice: SpeechSynthesizers.AnyVoiceName) throws + } + + public protocol _AnyVoiceNameConvertible { + func __conversion() throws -> SpeechSynthesizers.AnyVoiceName + } +} diff --git a/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift index 1a9f944f..3a85d5c6 100644 --- a/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift +++ b/Sources/CoreMI/Intramodular/TTS/SpeechSynthesizers.swift @@ -8,18 +8,3 @@ import Swift public enum SpeechSynthesizers { } - -extension SpeechSynthesizers { - public protocol VoiceName: Codable, Hashable, Identifiable { - - } -} - -extension SpeechSynthesizers { - public struct AnyVoiceName: SpeechSynthesizers.VoiceName { - public var id: AnyPersistentIdentifier - public var displayName: String - public var userInfo: UserInfo - } -} - From a91c9329900a86174f3f918061c159906c69c5fa Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 22 Nov 2024 17:41:40 -0700 Subject: [PATCH 28/73] NeetsAI + HumeAI --- Package.swift | 54 ++++++ .../ModelIdentifier.Provider.swift | 22 +++ .../CoreMI._ServiceVendorIdentifier.swift | 2 + ...umeAI.APISpecification.RequestBodies.swift | 76 +++++++++ .../API/HumeAI.APISpecification.swift | 156 ++++++++++++++++++ ...eAI.APISpeicification.ResponseBodies.swift | 57 +++++++ .../HumeAI/Intramodular/HumeAI.Client.swift | 125 ++++++++++++++ .../HumeAI/Intramodular/HumeAI.Model.swift | 22 +++ Sources/HumeAI/Intramodular/HumeAI.swift | 12 ++ .../Intramodular/Models/HumeAI.Voice.swift | 52 ++++++ Sources/HumeAI/module.swift | 7 + ...etsAI.APISpecification.RequestBodies.swift | 40 +++++ ...tsAI.APISpecification.ResponseBodies.swift | 14 ++ .../API/NeetsAI.APISpecification.swift | 137 +++++++++++++++ .../Models/NeetsAI.ChatMessage.swift | 59 +++++++ .../Intramodular/Models/NeetsAI.Voice.swift | 24 +++ .../NeetsAI/Intramodular/NeetsAI.Client.swift | 101 ++++++++++++ .../NeetsAI/Intramodular/NeetsAI.Model.swift | 50 ++++++ Sources/NeetsAI/Intramodular/NeetsAI.swift | 12 ++ Sources/NeetsAI/module.swift | 7 + ...Rime.APISpecification.ResponseBodies.swift | 2 +- Sources/Rime/module.swift | 2 + Tests/HumeAI/Intramodular/Tests.swift | 7 + Tests/HumeAI/module.swift | 7 + Tests/NeetsAI/Intramodular/Tests.swift | 140 ++++++++++++++++ Tests/NeetsAI/module.swift | 21 +++ 26 files changed, 1207 insertions(+), 1 deletion(-) create mode 100644 Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift create mode 100644 Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift create mode 100644 Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Model.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Voice.swift create mode 100644 Sources/HumeAI/module.swift create mode 100644 Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift create mode 100644 Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.ResponseBodies.swift create mode 100644 Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.swift create mode 100644 Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift create mode 100644 Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift create mode 100644 Sources/NeetsAI/Intramodular/NeetsAI.Client.swift create mode 100644 Sources/NeetsAI/Intramodular/NeetsAI.Model.swift create mode 100644 Sources/NeetsAI/Intramodular/NeetsAI.swift create mode 100644 Sources/NeetsAI/module.swift create mode 100644 Tests/HumeAI/Intramodular/Tests.swift create mode 100644 Tests/HumeAI/module.swift create mode 100644 Tests/NeetsAI/Intramodular/Tests.swift create mode 100644 Tests/NeetsAI/module.swift diff --git a/Package.swift b/Package.swift index 1cb9aa05..f2c02ee9 100644 --- a/Package.swift +++ b/Package.swift @@ -285,6 +285,36 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .target( + name: "HumeAI", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/HumeAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), + .target( + name: "NeetsAI", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/NeetsAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .target( name: "HuggingFace", dependencies: [ @@ -316,6 +346,8 @@ let package = Package( "Cohere", "TogetherAI", "HuggingFace", + "HumeAI", + "NeetsAI" ], path: "Sources/AI", swiftSettings: [ @@ -453,6 +485,28 @@ let package = Package( swiftSettings: [ .enableExperimentalFeature("AccessLevelOnImport") ] + ), + .testTarget( + name: "NeetsAITests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/NeetsAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), + .testTarget( + name: "HumeAITests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/HumeAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] ) ] ) diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index 126ac3ad..a4318232 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -26,6 +26,8 @@ extension ModelIdentifier { case _TogetherAI case _PlayHT case _Rime + case _HumeAI + case _NeetsAI case unknown(String) @@ -84,6 +86,14 @@ extension ModelIdentifier { public static var rime: Self { Self._Rime } + + public static var humeAI: Self { + self._HumeAI + } + + public static var neetsAI: Self { + self._NeetsAI + } } } @@ -124,6 +134,10 @@ extension ModelIdentifier.Provider: CustomStringConvertible { return "PlayHT" case ._Rime: return "Rime" + case ._HumeAI: + return "HumeAI" + case ._NeetsAI: + return "NeetsAI" case .unknown(let provider): return provider } @@ -165,6 +179,10 @@ extension ModelIdentifier.Provider: RawRepresentable { return "playht" case ._Rime: return "rime" + case ._HumeAI: + return "humeai" + case ._NeetsAI: + return "neetsai" case .unknown(let provider): return provider } @@ -202,6 +220,10 @@ extension ModelIdentifier.Provider: RawRepresentable { self = ._PlayHT case Self._Rime.rawValue: self = ._Rime + case Self._HumeAI.rawValue: + self = ._HumeAI + case Self._NeetsAI.rawValue: + self = ._NeetsAI default: self = .unknown(rawValue) } diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift index 02d956ba..8e1db394 100644 --- a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceVendorIdentifier.swift @@ -32,7 +32,9 @@ extension CoreMI._ServiceVendorIdentifier { public static let _Fal = CoreMI._ServiceVendorIdentifier(rawValue: "povar-firul-milij-jopat") public static let _Groq = CoreMI._ServiceVendorIdentifier(rawValue: "jabub-potuv-juniv-nodik") public static let _HuggingFace = CoreMI._ServiceVendorIdentifier(rawValue: "jutot-gugal-luzoh-vorig") + public static let _HumeAI = CoreMI._ServiceVendorIdentifier(rawValue: "kinot-tugug-rojum-sinis") public static let _Mistral = CoreMI._ServiceVendorIdentifier(rawValue: "vogas-fohig-mokij-titun") + public static let _NeetsAI = CoreMI._ServiceVendorIdentifier(rawValue: "tabut-fozak-tajah-bagaj") public static let _Ollama = CoreMI._ServiceVendorIdentifier(rawValue: "sotap-boris-navam-mitoh") public static let _OpenAI = CoreMI._ServiceVendorIdentifier(rawValue: "vodih-vakam-hiduz-tosob") public static let _Perplexity = CoreMI._ServiceVendorIdentifier(rawValue: "dohug-muboz-bopuz-kasar") diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..ec4d4271 --- /dev/null +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -0,0 +1,76 @@ +// +// HumeAI.APISpecification.RequestBodies.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.APISpecification { + enum RequestBodies { + struct ListVoicesInput: Codable { + let pageNumber: Int? + let pageSize: Int? + let name: String? + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case name + } + } + + struct CreateVoiceInput: Codable { + let name: String + let baseVoice: String + let parameterModel: String + let parameters: Parameters? + + private enum CodingKeys: String, CodingKey { + case name + case baseVoice = "base_voice" + case parameterModel = "parameter_model" + case parameters + } + + struct Parameters: Codable { + let gender: Double? + let articulation: Double? + let assertiveness: Double? + let buoyancy: Double? + let confidence: Double? + let enthusiasm: Double? + let nasality: Double? + let relaxedness: Double? + let smoothness: Double? + let tepidity: Double? + let tightness: Double? + } + } + + struct UpdateVoiceNameInput: Codable { + let name: String + } + + struct TTSInput: Codable { + let text: String + let voiceId: String + let speed: Double? + let stability: Double? + let similarityBoost: Double? + let styleExaggeration: Double? + + private enum CodingKeys: String, CodingKey { + case text + case voiceId = "voice_id" + case speed + case stability + case similarityBoost = "similarity_boost" + case styleExaggeration = "style_exaggeration" + } + } + } +} diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift new file mode 100644 index 00000000..5ded5111 --- /dev/null +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -0,0 +1,156 @@ +// +// HumeAI.APISpecification.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CorePersistence +import Diagnostics +import NetworkKit +import Swift +import SwiftAPI + +extension HumeAI { + public enum APIError: APIErrorProtocol { + public typealias API = HumeAI.APISpecification + + case apiKeyMissing + case audioDataError + case incorrectAPIKeyProvided + case rateLimitExceeded + case invalidContentType + case badRequest(request: API.Request?, error: API.Request.Error) + case unknown(message: String) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var host: URL + public var apiKey: String? + + public init( + host: URL = URL(string: "https://api.hume.ai")!, + apiKey: String? = nil + ) { + self.host = host + self.apiKey = apiKey + } + } + + public let configuration: Configuration + + public var host: URL { + configuration.host + } + + public var id: some Hashable { + configuration + } + + public init(configuration: Configuration) { + self.configuration = configuration + } + + // Custom Voice Endpoints + @GET + @Path("/v0/evi/custom_voices") + var listVoices = Endpoint() + + @POST + @Path("/v0/evi/custom_voices") + @Body(json: \.input) + var createVoice = Endpoint() + + @GET + @Path("/v0/evi/custom_voices/{id}") + var getVoice = Endpoint() + + @POST + @Path("/v0/evi/custom_voices/{id}") + @Body(json: \.input) + var createVoiceVersion = Endpoint() + + @DELETE + @Path("/v0/evi/custom_voices/{id}") + var deleteVoice = Endpoint() + + @PATCH + @Path("/v0/evi/custom_voices/{id}") + @Body(json: \.input) + var updateVoiceName = Endpoint() + + // Text to Speech Endpoints + @POST + @Path("/v0/tts/generate") + @Body(json: \.input) + var generateSpeech = Endpoint() + + @POST + @Path("/v0/tts/generate/stream") + @Body(json: \.input) + var generateSpeechStream = Endpoint() + } +} + +extension HumeAI.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + public override func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + var request: HTTPRequest = try super.buildRequestBase( + from: input, + context: context + ) + + guard let apiKey = context.root.configuration.apiKey, !apiKey.isEmpty else { + throw HumeAI.APIError.apiKeyMissing + } + + request = request + .header("Accept", "application/json") + .header(.authorization(.bearer, apiKey)) + .header(.contentType(.json)) + + return request + } + + public override func decodeOutputBase( + from response: HTTPResponse, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? HTTPRequest.Error { + if response.statusCode.rawValue == 401 { + apiError = .incorrectAPIKeyProvided + } else if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift new file mode 100644 index 00000000..b892a4f3 --- /dev/null +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -0,0 +1,57 @@ +// +// HumeAI.APISpeicification.ResponseBodies.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.APISpecification { + enum ResponseBodies { + struct VoiceList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let voices: [HumeAI.Voice] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case voices = "custom_voices_page" + } + } + + typealias Voice = HumeAI.Voice + + struct TTSOutput: Codable { + public let audio: Data + public let durationMs: Int + + private enum CodingKeys: String, CodingKey { + case audio + case durationMs = "duration_ms" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let base64String = try container.decode(String.self, forKey: .audio) + self.audio = try Data(base64Encoded: base64String).unwrap() + self.durationMs = try container.decode(Int.self, forKey: .durationMs) + } + } + + struct TTSStreamOutput: Codable { + public let streamURL: URL + public let durationMs: Int + + private enum CodingKeys: String, CodingKey { + case streamURL = "stream_url" + case durationMs = "duration_ms" + } + } + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client.swift b/Sources/HumeAI/Intramodular/HumeAI.Client.swift new file mode 100644 index 00000000..258192c9 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client.swift @@ -0,0 +1,125 @@ +// +// HumeAI.Clent.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension HumeAI { + @RuntimeDiscoverable + public final class Client: HTTPClient, _StaticSwift.Namespace { + public static var persistentTypeRepresentation: some IdentityRepresentation { + CoreMI._ServiceVendorIdentifier._HumeAI + } + + public typealias API = HumeAI.APISpecification + public typealias Session = HTTPSession + + public let interface: API + public let session: Session + public var sessionCache: EmptyKeyedCache + + public required init( + configuration: API.Configuration + ) { + self.interface = API(configuration: configuration) + self.session = HTTPSession.shared + self.sessionCache = .init() + } + + public convenience init( + apiKey: String + ) { + self.init(configuration: .init(apiKey: apiKey)) + } + } +} + +extension HumeAI.Client { + // Text to Speech + public func getAllAvailableVoices() async throws -> [HumeAI.Voice] { + let response = try await run(\.listVoices) + return response.voices + } + + public func generateSpeech( + text: String, + voiceID: String, + speed: Double? = nil, + stability: Double? = nil, + similarityBoost: Double? = nil, + styleExaggeration: Double? = nil + ) async throws -> Data { + let input = HumeAI.APISpecification.RequestBodies.TTSInput( + text: text, + voiceId: voiceID, + speed: speed, + stability: stability, + similarityBoost: similarityBoost, + styleExaggeration: styleExaggeration + ) + + return try await run(\.generateSpeech, with: input).audio + } + + public func generateSpeechStream( + text: String, + voiceID: String, + speed: Double? = nil, + stability: Double? = nil, + similarityBoost: Double? = nil, + styleExaggeration: Double? = nil + ) async throws -> Data { + let input = HumeAI.APISpecification.RequestBodies.TTSInput( + text: text, + voiceId: voiceID, + speed: speed, + stability: stability, + similarityBoost: similarityBoost, + styleExaggeration: styleExaggeration + ) + + let stream = try await run(\.generateSpeechStream, with: input) + + var request = URLRequest(url: stream.streamURL) + + let (audioData, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw HumeAI.APIError.audioDataError + } + + guard !audioData.isEmpty else { + throw HumeAI.APIError.audioDataError + } + + return audioData + } +} + +// MARK: - Conformances + +extension HumeAI.Client: _MIService { + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._HumeAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Model.swift b/Sources/HumeAI/Intramodular/HumeAI.Model.swift new file mode 100644 index 00000000..db0292d4 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Model.swift @@ -0,0 +1,22 @@ +// +// HumeAI.Model.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CoreMI +import CorePersistence +import Foundation +import Swift + +extension HumeAI { + public enum Model: String, Codable, Sendable { + case prosody = "prosody" + case language = "language" + case burst = "burst" + case face = "face" + case speech = "speech" + case tts = "tts" + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.swift b/Sources/HumeAI/Intramodular/HumeAI.swift new file mode 100644 index 00000000..3a9d2866 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.swift @@ -0,0 +1,12 @@ +// +// HumeAI.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Swift + +public enum HumeAI { + +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Voice.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Voice.swift new file mode 100644 index 00000000..4b053480 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Voice.swift @@ -0,0 +1,52 @@ +// +// HumeAI.Voice.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Foundation + +extension HumeAI { + public struct Voice: Hashable, Codable { + public var id: String + public var name: String + public var baseVoice: String + public var parameterModel: String + public var parameters: Parameters? + public var createdOn: Int64 + public var modifiedOn: Int64 + + public struct Parameters: Codable, Hashable { + public var gender: Double? + public var articulation: Double? + public var assertiveness: Double? + public var buoyancy: Double? + public var confidence: Double? + public var enthusiasm: Double? + public var nasality: Double? + public var relaxedness: Double? + public var smoothness: Double? + public var tepidity: Double? + public var tightness: Double? + } + + public init( + id: String, + name: String, + baseVoice: String, + parameterModel: String, + parameters: Parameters?, + createdOn: Int64, + modifiedOn: Int64 + ) { + self.id = id + self.name = name + self.baseVoice = baseVoice + self.parameterModel = parameterModel + self.parameters = parameters + self.createdOn = createdOn + self.modifiedOn = modifiedOn + } + } +} diff --git a/Sources/HumeAI/module.swift b/Sources/HumeAI/module.swift new file mode 100644 index 00000000..1c4d3b99 --- /dev/null +++ b/Sources/HumeAI/module.swift @@ -0,0 +1,7 @@ +// +// module.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + diff --git a/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..6c25fd1e --- /dev/null +++ b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift @@ -0,0 +1,40 @@ +// +// NeetsAI.APISpecification.RequestBodies.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +extension NeetsAI.APISpecification { + enum RequestBodies { + struct TTSInput: Codable { + let params: TTSParams + let text: String + let voiceId: String + + struct TTSParams: Codable { + let model: String + let temperature: Double + let diffusionIterations: Int + + private enum CodingKeys: String, CodingKey { + case model + case temperature + case diffusionIterations = "diffusion_iterations" + } + } + + private enum CodingKeys: String, CodingKey { + case params + case text + case voiceId = "voice_id" + } + } + + struct ChatInput: Codable { + let messages: [NeetsAI.ChatMessage] + let model: String + } + } +} + diff --git a/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.ResponseBodies.swift b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.ResponseBodies.swift new file mode 100644 index 00000000..4015c079 --- /dev/null +++ b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.ResponseBodies.swift @@ -0,0 +1,14 @@ +// +// NeetsAI.APISpecification.ResponseBodies.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Foundation + +extension NeetsAI.APISpecification { + enum ResponseBodies { + typealias ChatCompletion = NeetsAI.ChatCompletion + } +} diff --git a/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.swift b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.swift new file mode 100644 index 00000000..4850be83 --- /dev/null +++ b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.swift @@ -0,0 +1,137 @@ +// +// NeetsAI.APISpecification.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CorePersistence +import Diagnostics +import NetworkKit +import Swift +import SwiftAPI + +extension NeetsAI { + public enum APIError: APIErrorProtocol { + public typealias API = NeetsAI.APISpecification + + case apiKeyMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case invalidContentType + case badRequest(request: API.Request?, error: API.Request.Error) + case unknown(message: String) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var host: URL + public var apiKey: String? + + public init( + host: URL = URL(string: "https://api.neets.ai")!, + apiKey: String? = nil + ) { + self.host = host + self.apiKey = apiKey + } + } + + public let configuration: Configuration + + public var host: URL { + configuration.host + } + + public var id: some Hashable { + configuration + } + + public init(configuration: Configuration) { + self.configuration = configuration + } + + // Voice Management Endpoint + @GET + @Path("/v1/voices") + var listVoices = Endpoint() + + // Text to Speech Endpoint + @POST + @Path("/v1/tts") + @Body(json: \.input) + var generateSpeech = Endpoint() + + // Chat Completion Endpoint + @POST + @Path("/v1/chat/completions") + @Body(json: \.input) + var chatCompletion = Endpoint() + } +} + +extension NeetsAI.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + public override func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + var request: HTTPRequest = try super.buildRequestBase( + from: input, + context: context + ) + + guard let apiKey = context.root.configuration.apiKey, !apiKey.isEmpty else { + throw NeetsAI.APIError.apiKeyMissing + } + + request = request + .header("Accept", "application/json") + .header("X-API-Key", apiKey) + .header(.contentType(.json)) + + return request + } + + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? HTTPRequest.Error { + if response.statusCode.rawValue == 401 { + apiError = .incorrectAPIKeyProvided + } else if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + if Output.self == Data.self { + return response.data as! Output + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift new file mode 100644 index 00000000..05b57a75 --- /dev/null +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift @@ -0,0 +1,59 @@ +// +// NeetsAI.ChatMessage.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Foundation + +extension NeetsAI { + public struct ChatMessage: Codable, Hashable { + public let role: String + public let content: String + public let toolCalls: [String]? + + private enum CodingKeys: String, CodingKey { + case role + case content + case toolCalls = "tool_calls" + } + } + + public struct ChatCompletion: Codable { + public let id: String + public let object: String + public let created: Int + public let model: String + public let choices: [Choice] + public let usage: Usage + + public struct Choice: Codable { + public let index: Int + public let message: ChatMessage + public let logprobs: String? + public let finishReason: String? + public let stopReason: String? + + private enum CodingKeys: String, CodingKey { + case index + case message + case logprobs + case finishReason = "finish_reason" + case stopReason = "stop_reason" + } + } + + public struct Usage: Codable { + public let promptTokens: Int + public let totalTokens: Int + public let completionTokens: Int + + private enum CodingKeys: String, CodingKey { + case promptTokens = "prompt_tokens" + case totalTokens = "total_tokens" + case completionTokens = "completion_tokens" + } + } + } +} diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift new file mode 100644 index 00000000..c6c1c876 --- /dev/null +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift @@ -0,0 +1,24 @@ +// +// NeetsAI.Voice.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Foundation + +extension NeetsAI { + public struct Voice: Codable, Hashable { + public let id: String + public let title: String? + public let aliasOf: String? + public let supportedModels: [String] + + private enum CodingKeys: String, CodingKey { + case id + case title + case aliasOf = "alias_of" + case supportedModels = "supported_models" + } + } +} diff --git a/Sources/NeetsAI/Intramodular/NeetsAI.Client.swift b/Sources/NeetsAI/Intramodular/NeetsAI.Client.swift new file mode 100644 index 00000000..f366e38f --- /dev/null +++ b/Sources/NeetsAI/Intramodular/NeetsAI.Client.swift @@ -0,0 +1,101 @@ +// +// NeetsAI.Client.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension NeetsAI { + @RuntimeDiscoverable + public final class Client: HTTPClient, _StaticSwift.Namespace { + public static var persistentTypeRepresentation: some IdentityRepresentation { + CoreMI._ServiceVendorIdentifier._NeetsAI + } + + public typealias API = NeetsAI.APISpecification + public typealias Session = HTTPSession + + public let interface: API + public let session: Session + public var sessionCache: EmptyKeyedCache + + public required init( + configuration: API.Configuration + ) { + self.interface = API(configuration: configuration) + self.session = HTTPSession.shared + self.sessionCache = .init() + } + + public convenience init( + apiKey: String + ) { + self.init(configuration: .init(apiKey: apiKey)) + } + } +} + +extension NeetsAI.Client { + public func getAllAvailableVoices() async throws -> [NeetsAI.Voice] { + try await run(\.listVoices) + } + + public func generateSpeech( + text: String, + voiceId: String, + model: NeetsAI.Model = .arDiff50k, + temperature: Double = 1, + diffusionIterations: Int = 5 + ) async throws -> Data { + let input = API.RequestBodies.TTSInput( + params: .init( + model: model.rawValue, + temperature: temperature, + diffusionIterations: diffusionIterations + ), + text: text, + voiceId: voiceId + ) + + return try await run(\.generateSpeech, with: input) + } + + public func chat( + messages: [NeetsAI.ChatMessage], + model: NeetsAI.Model = .mistralai + ) async throws -> NeetsAI.ChatCompletion { + let input = API.RequestBodies.ChatInput( + messages: messages, + model: model.rawValue + ) + + return try await run(\.chatCompletion, with: input) + } +} + +// MARK: - Conformances + +extension NeetsAI.Client: _MIService { + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._NeetsAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} diff --git a/Sources/NeetsAI/Intramodular/NeetsAI.Model.swift b/Sources/NeetsAI/Intramodular/NeetsAI.Model.swift new file mode 100644 index 00000000..5400d95b --- /dev/null +++ b/Sources/NeetsAI/Intramodular/NeetsAI.Model.swift @@ -0,0 +1,50 @@ +// +// NeetsAI.Model.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import CoreMI +import CorePersistence +import Foundation +import Swift + +extension NeetsAI { + public enum Model: String, Codable, Sendable { + case arDiff50k = "ar-diff-50k" + case styleDiff500 = "style-diff-500" + case vits = "vits" + case mistralai = "mistralai/Mixtral-8X7B-Instruct-v0.1" + } +} + +// MARK: - Model Conformances + +extension NeetsAI.Model: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension NeetsAI.Model: ModelIdentifierRepresentable { + public init(from identifier: ModelIdentifier) throws { + guard identifier.provider == ._NeetsAI, identifier.revision == nil else { + throw Never.Reason.illegal + } + + guard let model = Self(rawValue: identifier.name) else { + throw Never.Reason.unexpected + } + + self = model + } + + public func __conversion() throws -> ModelIdentifier { + ModelIdentifier( + provider: ._NeetsAI, + name: rawValue, + revision: nil + ) + } +} diff --git a/Sources/NeetsAI/Intramodular/NeetsAI.swift b/Sources/NeetsAI/Intramodular/NeetsAI.swift new file mode 100644 index 00000000..33d9eefa --- /dev/null +++ b/Sources/NeetsAI/Intramodular/NeetsAI.swift @@ -0,0 +1,12 @@ +// +// NeetsAI.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import Swift + +public enum NeetsAI { + +} diff --git a/Sources/NeetsAI/module.swift b/Sources/NeetsAI/module.swift new file mode 100644 index 00000000..1c4d3b99 --- /dev/null +++ b/Sources/NeetsAI/module.swift @@ -0,0 +1,7 @@ +// +// module.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + diff --git a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift index a1889a49..f9dd427f 100644 --- a/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift +++ b/Sources/Rime/Intramodular/API/Rime.APISpecification.ResponseBodies.swift @@ -57,5 +57,5 @@ extension Rime.APISpecification { try container.encode(base64String, forKey: .audioContent) } } - } + } } diff --git a/Sources/Rime/module.swift b/Sources/Rime/module.swift index c7efeae6..698a288f 100644 --- a/Sources/Rime/module.swift +++ b/Sources/Rime/module.swift @@ -5,3 +5,5 @@ // Created by Jared Davidson on 11/21/24. // +@_exported import Swallow +@_exported import SwallowMacrosClient diff --git a/Tests/HumeAI/Intramodular/Tests.swift b/Tests/HumeAI/Intramodular/Tests.swift new file mode 100644 index 00000000..f09e84e7 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Tests.swift @@ -0,0 +1,7 @@ +// +// Tests.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + diff --git a/Tests/HumeAI/module.swift b/Tests/HumeAI/module.swift new file mode 100644 index 00000000..1c4d3b99 --- /dev/null +++ b/Tests/HumeAI/module.swift @@ -0,0 +1,7 @@ +// +// module.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + diff --git a/Tests/NeetsAI/Intramodular/Tests.swift b/Tests/NeetsAI/Intramodular/Tests.swift new file mode 100644 index 00000000..0ef6f136 --- /dev/null +++ b/Tests/NeetsAI/Intramodular/Tests.swift @@ -0,0 +1,140 @@ +// +// Untitled.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import XCTest +import AI +import NetworkKit +import SwiftAPI +@testable import NeetsAI + +final class NeetsAIClientTests: XCTestCase { + + // MARK: - Voice Tests + + func test_getAllAvailableVoices_returnsVoicesList() async throws { + // Given + let mockVoices = [ + NeetsAI.Voice(id: "vits-ben-14", title: nil, aliasOf: nil, supportedModels: ["vits"]), + NeetsAI.Voice(id: "vits-eng-2", title: nil, aliasOf: nil, supportedModels: ["vits"]) + ] + + // When + let voices = try await client.getAllAvailableVoices() + + // Then + XCTAssertEqual(voices.count, 2) + XCTAssertEqual(voices.map(\.id), mockVoices.map(\.id)) + XCTAssertEqual(voices.first?.supportedModels, ["vits"]) + } + + // MARK: - Text to Speech Tests + + func test_generateSpeech_withDefaultParameters_returnsAudioData() async throws { + // Given + let text = "Hello, world!" + let voiceId = "vits-ben-14" + + // When + let audioData = try await client.generateSpeech( + text: text, + voiceId: voiceId + ) + + // Then + XCTAssertFalse(audioData.isEmpty) + } + + func test_generateSpeech_withCustomParameters_returnsAudioData() async throws { + // Given + let text = "Test speech" + let voiceId = "vits-ben-14" + let model = NeetsAI.Model.arDiff50k + let temperature = 0.8 + let diffusionIterations = 10 + + // When + let audioData = try await client.generateSpeech( + text: text, + voiceId: voiceId, + model: model, + temperature: temperature, + diffusionIterations: diffusionIterations + ) + + // Then + XCTAssertFalse(audioData.isEmpty) + } + + func test_generateSpeech_withInvalidVoiceId_throwsError() async { + // Given + let text = "Test speech" + let invalidVoiceId = "invalid-voice" + + // Then + do { + _ = try await client.generateSpeech(text: text, voiceId: invalidVoiceId) + XCTFail("Expected error to be thrown") + } catch { + XCTAssertTrue(error is NeetsAI.APIError) + } + } + + // MARK: - Chat Tests + + func test_chat_withDefaultModel_returnsCompletion() async throws { + // Given + let messages = [ + NeetsAI.ChatMessage(role: "user", content: "Hello", toolCalls: nil) + ] + + // When + let completion = try await client.chat(messages: messages) + + // Then + XCTAssertFalse(completion.id.isEmpty) + XCTAssertEqual(completion.object, "chat.completion") + XCTAssertGreaterThan(completion.created, 0) + XCTAssertEqual(completion.model, NeetsAI.Model.mistralai.rawValue) + + XCTAssertFalse(completion.choices.isEmpty) + let firstChoice = try XCTUnwrap(completion.choices.first) + XCTAssertEqual(firstChoice.index, 0) + XCTAssertEqual(firstChoice.message.role, "assistant") + XCTAssertFalse(firstChoice.message.content.isEmpty) + + XCTAssertGreaterThan(completion.usage.promptTokens, 0) + XCTAssertGreaterThan(completion.usage.completionTokens, 0) + XCTAssertGreaterThan(completion.usage.totalTokens, 0) + } + + func test_chat_withCustomModel_usesSpecifiedModel() async throws { + // Given + let messages = [ + NeetsAI.ChatMessage(role: "user", content: "Test", toolCalls: nil) + ] + let model = NeetsAI.Model.mistralai + + // When + let completion = try await client.chat(messages: messages, model: model) + + // Then + XCTAssertEqual(completion.model, model.rawValue) + } + + func test_chat_withEmptyMessages_throwsError() async { + // Given + let emptyMessages: [NeetsAI.ChatMessage] = [] + + // Then + do { + _ = try await client.chat(messages: emptyMessages) + XCTFail("Expected error to be thrown") + } catch { + XCTAssertTrue(error is NeetsAI.APIError) + } + } +} diff --git a/Tests/NeetsAI/module.swift b/Tests/NeetsAI/module.swift new file mode 100644 index 00000000..147ed769 --- /dev/null +++ b/Tests/NeetsAI/module.swift @@ -0,0 +1,21 @@ +// +// module.swift +// AI +// +// Created by Jared Davidson on 11/22/24. +// + +import NeetsAI + +public var NEETSAI_API_KEY: String { + "59fd70d014324dfe9100c8d3daefd84c" +} + +public var client: NeetsAI.Client { + let client = NeetsAI.Client( + apiKey: NEETSAI_API_KEY + ) + + return client +} + From 05f6319f0622a858459af2aabbc222e108e021b6 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 22 Nov 2024 17:48:37 -0700 Subject: [PATCH 29/73] update tests --- .../Models/NeetsAI.ChatMessage.swift | 12 +++---- .../Intramodular/Models/NeetsAI.Voice.swift | 7 ---- Tests/NeetsAI/Intramodular/Tests.swift | 35 ++----------------- 3 files changed, 9 insertions(+), 45 deletions(-) diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift index 05b57a75..e39b56bf 100644 --- a/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.ChatMessage.swift @@ -16,7 +16,7 @@ extension NeetsAI { private enum CodingKeys: String, CodingKey { case role case content - case toolCalls = "tool_calls" + case toolCalls } } @@ -39,8 +39,8 @@ extension NeetsAI { case index case message case logprobs - case finishReason = "finish_reason" - case stopReason = "stop_reason" + case finishReason + case stopReason } } @@ -50,9 +50,9 @@ extension NeetsAI { public let completionTokens: Int private enum CodingKeys: String, CodingKey { - case promptTokens = "prompt_tokens" - case totalTokens = "total_tokens" - case completionTokens = "completion_tokens" + case promptTokens + case totalTokens + case completionTokens } } } diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift index c6c1c876..2f035154 100644 --- a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift @@ -13,12 +13,5 @@ extension NeetsAI { public let title: String? public let aliasOf: String? public let supportedModels: [String] - - private enum CodingKeys: String, CodingKey { - case id - case title - case aliasOf = "alias_of" - case supportedModels = "supported_models" - } } } diff --git a/Tests/NeetsAI/Intramodular/Tests.swift b/Tests/NeetsAI/Intramodular/Tests.swift index 0ef6f136..4ccae19f 100644 --- a/Tests/NeetsAI/Intramodular/Tests.swift +++ b/Tests/NeetsAI/Intramodular/Tests.swift @@ -26,9 +26,7 @@ final class NeetsAIClientTests: XCTestCase { let voices = try await client.getAllAvailableVoices() // Then - XCTAssertEqual(voices.count, 2) - XCTAssertEqual(voices.map(\.id), mockVoices.map(\.id)) - XCTAssertEqual(voices.first?.supportedModels, ["vits"]) + XCTAssertEqual(voices.isEmpty, false) } // MARK: - Text to Speech Tests @@ -36,7 +34,7 @@ final class NeetsAIClientTests: XCTestCase { func test_generateSpeech_withDefaultParameters_returnsAudioData() async throws { // Given let text = "Hello, world!" - let voiceId = "vits-ben-14" + let voiceId = "us-male-13" // When let audioData = try await client.generateSpeech( @@ -51,7 +49,7 @@ final class NeetsAIClientTests: XCTestCase { func test_generateSpeech_withCustomParameters_returnsAudioData() async throws { // Given let text = "Test speech" - let voiceId = "vits-ben-14" + let voiceId = "us-male-13" let model = NeetsAI.Model.arDiff50k let temperature = 0.8 let diffusionIterations = 10 @@ -69,20 +67,6 @@ final class NeetsAIClientTests: XCTestCase { XCTAssertFalse(audioData.isEmpty) } - func test_generateSpeech_withInvalidVoiceId_throwsError() async { - // Given - let text = "Test speech" - let invalidVoiceId = "invalid-voice" - - // Then - do { - _ = try await client.generateSpeech(text: text, voiceId: invalidVoiceId) - XCTFail("Expected error to be thrown") - } catch { - XCTAssertTrue(error is NeetsAI.APIError) - } - } - // MARK: - Chat Tests func test_chat_withDefaultModel_returnsCompletion() async throws { @@ -124,17 +108,4 @@ final class NeetsAIClientTests: XCTestCase { // Then XCTAssertEqual(completion.model, model.rawValue) } - - func test_chat_withEmptyMessages_throwsError() async { - // Given - let emptyMessages: [NeetsAI.ChatMessage] = [] - - // Then - do { - _ = try await client.chat(messages: emptyMessages) - XCTFail("Expected error to be thrown") - } catch { - XCTAssertTrue(error is NeetsAI.APIError) - } - } } From 17e66c5bc94b1960a9971c03561e8a7fd37a607c Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 22 Nov 2024 17:55:11 -0700 Subject: [PATCH 30/73] removed unused --- .../API/HumeAI.APISpecification.swift | 11 ---- .../HumeAI/Intramodular/HumeAI.Client.swift | 54 ------------------- 2 files changed, 65 deletions(-) diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 5ded5111..df3ccd66 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -86,17 +86,6 @@ extension HumeAI { @Path("/v0/evi/custom_voices/{id}") @Body(json: \.input) var updateVoiceName = Endpoint() - - // Text to Speech Endpoints - @POST - @Path("/v0/tts/generate") - @Body(json: \.input) - var generateSpeech = Endpoint() - - @POST - @Path("/v0/tts/generate/stream") - @Body(json: \.input) - var generateSpeechStream = Endpoint() } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client.swift b/Sources/HumeAI/Intramodular/HumeAI.Client.swift index 258192c9..2adafb8f 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client.swift @@ -47,60 +47,6 @@ extension HumeAI.Client { let response = try await run(\.listVoices) return response.voices } - - public func generateSpeech( - text: String, - voiceID: String, - speed: Double? = nil, - stability: Double? = nil, - similarityBoost: Double? = nil, - styleExaggeration: Double? = nil - ) async throws -> Data { - let input = HumeAI.APISpecification.RequestBodies.TTSInput( - text: text, - voiceId: voiceID, - speed: speed, - stability: stability, - similarityBoost: similarityBoost, - styleExaggeration: styleExaggeration - ) - - return try await run(\.generateSpeech, with: input).audio - } - - public func generateSpeechStream( - text: String, - voiceID: String, - speed: Double? = nil, - stability: Double? = nil, - similarityBoost: Double? = nil, - styleExaggeration: Double? = nil - ) async throws -> Data { - let input = HumeAI.APISpecification.RequestBodies.TTSInput( - text: text, - voiceId: voiceID, - speed: speed, - stability: stability, - similarityBoost: similarityBoost, - styleExaggeration: styleExaggeration - ) - - let stream = try await run(\.generateSpeechStream, with: input) - - var request = URLRequest(url: stream.streamURL) - - let (audioData, response) = try await URLSession.shared.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { - throw HumeAI.APIError.audioDataError - } - - guard !audioData.isEmpty else { - throw HumeAI.APIError.audioDataError - } - - return audioData - } } // MARK: - Conformances From 229088e06b7ce0ba7854e04374a0b49c8bda08a5 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 10:33:53 -0700 Subject: [PATCH 31/73] HumeAI Endpoints --- ...umeAI.APISpecification.RequestBodies.swift | 198 ++++++++++ .../API/HumeAI.APISpecification.swift | 318 ++++++++++++++- ...eAI.APISpeicification.ResponseBodies.swift | 364 ++++++++++++++++++ .../Intramodular/HumeAI.Client-Batch.swift | 28 ++ .../Intramodular/HumeAI.Client-Chat.swift | 27 ++ .../HumeAI.Client-ChatGroups.swift | 21 + .../Intramodular/HumeAI.Client-Chats.swift | 26 ++ .../Intramodular/HumeAI.Client-Configs.swift | 34 ++ .../HumeAI.Client-CustomVoices.swift | 34 ++ .../Intramodular/HumeAI.Client-Datasets.swift | 34 ++ .../Intramodular/HumeAI.Client-Files.swift | 30 ++ .../Intramodular/HumeAI.Client-Jobs.swift | 40 ++ .../Intramodular/HumeAI.Client-Models.swift | 33 ++ .../Intramodular/HumeAI.Client-Prompts.swift | 35 ++ .../Intramodular/HumeAI.Client-Stream.swift | 21 + .../Intramodular/HumeAI.Client-Tools.swift | 39 ++ 16 files changed, 1269 insertions(+), 13 deletions(-) create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index ec4d4271..1d22ce42 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -72,5 +72,203 @@ extension HumeAI.APISpecification { case styleExaggeration = "style_exaggeration" } } + + struct BatchInferenceJobInput: Codable { + let files: [FileInput] + let models: [HumeAI.Model] + let callback: CallbackConfig? + } + + struct FileInput: Codable { + let url: String + let mimeType: String + let metadata: [String: String]? + + private enum CodingKeys: String, CodingKey { + case url + case mimeType = "mime_type" + case metadata + } + } + + struct CallbackConfig: Codable { + let url: String + let metadata: [String: String]? + } + + struct ChatRequest: Codable { + let messages: [Message] + let model: String + let temperature: Double? + let maxTokens: Int? + let stream: Bool? + + struct Message: Codable { + let role: String + let content: String + } + + private enum CodingKeys: String, CodingKey { + case messages, model, temperature + case maxTokens = "max_tokens" + case stream + } + } + + struct CreateConfigInput: Codable { + let name: String + let description: String? + let settings: [String: String] + } + + struct UpdateConfigNameInput: Codable { + let name: String + } + + struct UpdateConfigDescriptionInput: Codable { + let description: String + } + + struct CreateDatasetInput: Codable { + let name: String + let description: String? + let fileIds: [String] + + private enum CodingKeys: String, CodingKey { + case name, description + case fileIds = "file_ids" + } + } + + struct UploadFileInput: Codable, HTTPRequest.Multipart.ContentConvertible { + let file: Data + let name: String + let metadata: [String: String]? + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result: HTTPRequest.Multipart.Content = .init() + result.append( + .file( + named: "file", + data: file, + filename: name, + contentType: .json + ) + ) + if let metadata = metadata { + result.append( + .string( + named: "metadata", + value: try JSONEncoder().encode(metadata).utf8String ?? "" + ) + ) + } + return result + } + } + + struct UpdateFileNameInput: Codable { + let name: String + } + + struct TrainingJobInput: Codable { + let datasetId: String + let name: String + let description: String? + let configuration: [String: String] + + private enum CodingKeys: String, CodingKey { + case datasetId = "dataset_id" + case name, description, configuration + } + } + + struct CustomInferenceJobInput: Codable { + let modelId: String + let files: [FileInput] + let configuration: [String: String] + + private enum CodingKeys: String, CodingKey { + case modelId = "model_id" + case files, configuration + } + } + + struct UpdateModelNameInput: Codable { + let name: String + } + + struct UpdateModelDescriptionInput: Codable { + let description: String + } + + struct CreatePromptInput: Codable { + let name: String + let description: String? + let content: String + let metadata: [String: String]? + } + + struct UpdatePromptNameInput: Codable { + let name: String + } + + struct UpdatePromptDescriptionInput: Codable { + let description: String + } + + struct StreamInput: Codable, HTTPRequest.Multipart.ContentConvertible { + let file: Data + let models: [HumeAI.Model] + let metadata: [String: String]? + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result: HTTPRequest.Multipart.Content = .init() + + result.append( + .file( + named: "file", + data: file, + filename: "file", + contentType: .binary + ) + ) + result.append( + .string( + named: "models", + value: try JSONEncoder().encode(models).utf8String ?? "" + ) + ) + + if let metadata = metadata { + result.append( + .string( + named: "metadata", + value: try JSONEncoder().encode(metadata).utf8String ?? "" + ) + ) + } + + return result + } + } + + struct CreateToolInput: Codable { + let name: String + let description: String? + let configuration: Configuration + + struct Configuration: Codable { + let parameters: [String: String] + } + } + + struct UpdateToolNameInput: Codable { + let name: String + } + + struct UpdateToolDescriptionInput: Codable { + let description: String + } } } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index df3ccd66..bc7496e9 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -59,37 +59,329 @@ extension HumeAI { self.configuration = configuration } - // Custom Voice Endpoints + // MARK: - Tools @GET - @Path("/v0/evi/custom_voices") - var listVoices = Endpoint() + @Path("/v0/tools") + var listTools = Endpoint() @POST - @Path("/v0/evi/custom_voices") + @Path("/v0/tools") @Body(json: \.input) - var createVoice = Endpoint() + var createTool = Endpoint() @GET - @Path("/v0/evi/custom_voices/{id}") - var getVoice = Endpoint() + @Path({ context -> String in "/v0/tools/\(context.input.id)/versions" }) + var listToolVersions = Endpoint() @POST - @Path("/v0/evi/custom_voices/{id}") + @Path({ context -> String in "/v0/tools/\(context.input.id)/versions" }) @Body(json: \.input) - var createVoiceVersion = Endpoint() + var createToolVersion = Endpoint() @DELETE - @Path("/v0/evi/custom_voices/{id}") - var deleteVoice = Endpoint() + @Path({ context -> String in "/v0/tools/\(context.input.id)" }) + var deleteTool = Endpoint() @PATCH - @Path("/v0/evi/custom_voices/{id}") + @Path({ context -> String in "/v0/tools/\(context.input.id)" }) @Body(json: \.input) - var updateVoiceName = Endpoint() + var updateToolName = Endpoint() + + @GET + @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + var getToolVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + var deleteToolVersion = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + @Body(json: \.input) + var updateToolDescription = Endpoint() + + // MARK: - Prompts + @GET + @Path("/v0/prompts") + var listPrompts = Endpoint() + + @POST + @Path("/v0/prompts") + @Body(json: \.input) + var createPrompt = Endpoint() + + @GET + @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions" }) + var listPromptVersions = Endpoint() + + @POST + @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions" }) + @Body(json: \.input) + var createPromptVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/prompts/\(context.input.id)" }) + var deletePrompt = Endpoint() + + @PATCH + @Path( + { + context -> String in "/v0/prompts/\(context.input.id)" + }) + @Body(json: \.input) + var updatePromptName = Endpoint() + + @GET + @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) + var getPromptVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) + var deletePromptVersion = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) + @Body(json: \.input) + var updatePromptDescription = Endpoint() + + // MARK: - Custom Voices + @GET + @Path("/v0/custom-voices") + var listCustomVoices = Endpoint() + + @POST + @Path("/v0/custom-voices") + @Body(json: \.input) + var createCustomVoice = Endpoint() + + @GET + @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + var getCustomVoice = Endpoint() + + @POST + @Path({ context -> String in "/v0/custom-voices/\(context.input.id)/versions" }) + @Body(json: \.input) + var createCustomVoiceVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + var deleteCustomVoice = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + @Body(json: \.input) + var updateCustomVoiceName = Endpoint() + + // MARK: - Configs + @GET + @Path("/v0/configs") + var listConfigs = Endpoint() + + @POST + @Path("/v0/configs") + @Body(json: \.input) + var createConfig = Endpoint() + + @GET + @Path({ context -> String in "/v0/configs/\(context.input.id)/versions" }) + var listConfigVersions = Endpoint() + + @POST + @Path({ context -> String in "/v0/configs/\(context.input.id)/versions" }) + @Body(json: \.input) + var createConfigVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/configs/\(context.input.id)" }) + var deleteConfig = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/configs/\(context.input.id)" }) + @Body(json: \.input) + var updateConfigName = Endpoint() + + @GET + @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + var getConfigVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + var deleteConfigVersion = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + @Body(json: \.input) + var updateConfigDescription = Endpoint() + + // MARK: - Chats + @GET + @Path("/v0/chats") + var listChats = Endpoint() + + @GET + @Path({ context -> String in "/v0/chats/\(context.input.id)/events" }) + var listChatEvents = Endpoint() + + @GET + @Path({ context -> String in "/v0/chats/\(context.input.id)/audio" }) + var getChatAudio = Endpoint() + + // MARK: - Chat Groups + @GET + @Path("/v0/chat-groups") + var listChatGroups = Endpoint() + + @GET + @Path({ context -> String in "/v0/chat-groups/\(context.input.id)" }) + var getChatGroup = Endpoint() + + @GET + @Path({ context -> String in "/v0/chat-groups/\(context.input.id)/events" }) + var listChatGroupEvents = Endpoint() + + @GET + @Path({ context -> String in "/v0/chat-groups/\(context.input.id)/audio" }) + var getChatGroupAudio = Endpoint() + + // MARK: - Chat + @POST + @Path("/v0/chat") + @Body(json: \.input) + var chat = Endpoint() + + // MARK: - Batch + @GET + @Path("/v0/batch/jobs") + var listJobs = Endpoint() + + @POST + @Path("/v0/batch/jobs") + @Body(json: \.input) + var startInferenceJob = Endpoint() + + @GET + @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)" }) + var getJobDetails = Endpoint() + + @GET + @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)/predictions" }) + var getJobPredictions = Endpoint() + + @GET + @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)/artifacts" }) + var getJobArtifacts = Endpoint() + + // MARK: - Stream + @POST + @Path("/v0/stream") + @Body(multipart: .input) + var streamInference = Endpoint() + + // MARK: - Files + @GET + @Path("/v0/files") + var listFiles = Endpoint() + + @POST + @Path("/v0/files") + @Body(multipart: .input) + var uploadFile = Endpoint() + + @GET + @Path({ context -> String in "/v0/files/\(context.input.id)" }) + var getFile = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/files/\(context.input.id)" }) + var deleteFile = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/files/\(context.input.id)" }) + @Body(json: \.input) + var updateFileName = Endpoint() + + @GET + @Path({ context -> String in "/v0/files/\(context.input.id)/predictions" }) + var getFilePredictions = Endpoint() + + // MARK: - Datasets + @GET + @Path("/v0/datasets") + var listDatasets = Endpoint() + + @POST + @Path("/v0/datasets") + @Body(json: \.input) + var createDataset = Endpoint() + + @GET + @Path({ context -> String in "/v0/datasets/\(context.input.id)" }) + var getDataset = Endpoint() + + @POST + @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) + @Body(json: \.input) + var createDatasetVersion = Endpoint() + + @DELETE + @Path({ context -> String in "/v0/datasets/\(context.input.id)" }) + var deleteDataset = Endpoint() + + @GET + @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) + var listDatasetVersions = Endpoint() + // MARK: - Models + @GET + @Path("/v0/models") + var listModels = Endpoint() + + @GET + @Path({ context -> String in "/v0/models/\(context.input.id)" }) + var getModel = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/models/\(context.input.id)" }) + @Body(json: \.input) + var updateModelName = Endpoint() + + @GET + @Path({ context -> String in "/v0/models/\(context.input.id)/versions" }) + var listModelVersions = Endpoint() + + @GET + @Path({ context -> String in "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" }) + var getModelVersion = Endpoint() + + @PATCH + @Path({ context -> String in "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" }) + @Body(json: \.input) + var updateModelDescription = Endpoint() + + // MARK: - Jobs + @POST + @Path("/v0/jobs/training") + @Body(json: \.input) + var startTrainingJob = Endpoint() + + @POST + @Path("/v0/jobs/inference") + @Body(json: \.input) + var startCustomInferenceJob = Endpoint() } } extension HumeAI.APISpecification { + enum PathInput { + struct ID: Codable { + let id: String + } + + struct IDWithVersion: Codable { + let id: String + let versionId: String + } + } + public final class Endpoint: BaseHTTPEndpoint { public override func buildRequestBase( from input: Input, diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index b892a4f3..997b36f6 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -53,5 +53,369 @@ extension HumeAI.APISpecification { case durationMs = "duration_ms" } } + + struct ChatResponse: Codable { + let id: String + let created: Int64 + let choices: [Choice] + let usage: Usage + + struct Choice: Codable { + let index: Int + let message: Message + let finishReason: String? + + struct Message: Codable { + let role: String + let content: String + } + + private enum CodingKeys: String, CodingKey { + case index, message + case finishReason = "finish_reason" + } + } + + struct Usage: Codable { + let promptTokens: Int + let completionTokens: Int + let totalTokens: Int + + private enum CodingKeys: String, CodingKey { + case promptTokens = "prompt_tokens" + case completionTokens = "completion_tokens" + case totalTokens = "total_tokens" + } + } + } + + struct ChatGroup: Codable { + let id: String + let name: String + let createdOn: Int64 + let modifiedOn: Int64 + let chats: [Chat]? + } + + struct ChatGroupList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let chatGroups: [ChatGroup] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case chatGroups = "chat_groups_page" + } + } + + struct Chat: Codable { + let id: String + let name: String + let createdOn: Int64 + let modifiedOn: Int64 + } + + struct ChatEvent: Codable { + let id: String + let chatId: String + let type: String + let content: String + let createdOn: Int64 + let audioUrl: String? + let metadata: [String: String]? + } + + struct ChatList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let chats: [Chat] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case chats = "chats_page" + } + } + + struct ChatEventList: Codable { + let events: [ChatEvent] + } + + typealias ChatAudio = Data + + struct Config: Codable { + let id: String + let name: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let versions: [ConfigVersion]? + } + + struct ConfigVersion: Codable { + let id: String + let configId: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let settings: [String: String] + } + + struct ConfigList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let configs: [Config] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case configs = "configs_page" + } + } + + struct VoiceParameters: Codable { + let gender: Double? + let articulation: Double? + let assertiveness: Double? + let buoyancy: Double? + let confidence: Double? + let enthusiasm: Double? + let nasality: Double? + let relaxedness: Double? + let smoothness: Double? + let tepidity: Double? + let tightness: Double? + } + + struct CustomVoiceList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let voices: [Voice] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case voices = "voices_page" + } + } + + struct Dataset: Codable { + let id: String + let name: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let versions: [DatasetVersion]? + } + + struct DatasetVersion: Codable { + let id: String + let datasetId: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let files: [File]? + } + + struct DatasetList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let datasets: [Dataset] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case datasets = "datasets_page" + } + } + + struct File: Codable { + let id: String + let name: String + let size: Int + let mimeType: String + let createdOn: Int64 + let modifiedOn: Int64 + let metadata: [String: String]? + + private enum CodingKeys: String, CodingKey { + case id, name, size + case mimeType = "mime_type" + case createdOn = "created_on" + case modifiedOn = "modified_on" + case metadata + } + } + + struct FileList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let files: [File] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case files = "files_page" + } + } + + struct ModelVersion: Codable { + let id: String + let modelId: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let configuration: [String: String] + } + + struct Model: Codable { + let id: String + let name: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let versions: [ModelVersion]? + } + + struct ModelList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let models: [HumeAI.Model] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case models = "models_page" + } + } + + struct Prompt: Codable { + let id: String + let name: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let versions: [PromptVersion]? + } + + struct PromptVersion: Codable { + let id: String + let promptId: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let content: String + let metadata: [String: String]? + } + + struct PromptList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let prompts: [Prompt] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case prompts = "prompts_page" + } + } + + struct Tool: Codable { + let id: String + let name: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let versions: [ToolVersion]? + } + + struct ToolVersion: Codable { + let id: String + let toolId: String + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 + let configuration: Configuration + + struct Configuration: Codable { + let parameters: [String: String] + } + } + + struct ToolList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let tools: [Tool] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case tools = "tools_page" + } + } + + struct Job: Codable { + let id: String + let status: String + let createdOn: Int64 + let modifiedOn: Int64 + let predictions: [Prediction]? + let artifacts: [String: String]? + + struct Prediction: Codable { + let file: FileInfo + let results: [ModelResult] + + struct FileInfo: Codable { + let url: String + let mimeType: String + let metadata: [String: String]? + + private enum CodingKeys: String, CodingKey { + case url + case mimeType = "mime_type" + case metadata + } + } + + struct ModelResult: Codable { + let model: String + let results: [String: String] + } + } + } + + struct JobList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let jobs: [Job] + + private enum CodingKeys: String, CodingKey { + case pageNumber = "page_number" + case pageSize = "page_size" + case totalPages = "total_pages" + case jobs = "jobs_page" + } + } } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift new file mode 100644 index 00000000..e0ac8101 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift @@ -0,0 +1,28 @@ +// +// HumeAI.Client-Jobs.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func startInferenceJob( + files: [HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput.FileInput], + models: [HumeAI.Model] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + let input = HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput( + files: files, + models: models, + callback: nil + ) + return try await run(\.startInferenceJob, with: input) + } + + public func getJobDetails(id: String) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + try await run(\.getJobDetails, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift new file mode 100644 index 00000000..dc011978 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift @@ -0,0 +1,27 @@ +// +// HumeAI.Client-Chat.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func chat( + messages: [HumeAI.APISpecification.RequestBodies.ChatRequest.Message], + model: String, + temperature: Double? = nil + ) async throws -> HumeAI.APISpecification.ResponseBodies.ChatResponse { + let input = HumeAI.APISpecification.RequestBodies.ChatRequest( + messages: messages, + model: model, + temperature: temperature, + maxTokens: nil, + stream: nil + ) + return try await run(\.chat, with: input) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift new file mode 100644 index 00000000..c3683617 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift @@ -0,0 +1,21 @@ +// +// HumeAI.Client.ChatGroup.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listChatGroups() async throws -> [HumeAI.APISpecification.ResponseBodies.ChatGroup] { + let response = try await run(\.listChatGroups) + return response.chatGroups + } + + public func getChatGroup(id: String) async throws -> HumeAI.APISpecification.ResponseBodies.ChatGroup { + try await run(\.getChatGroup, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift new file mode 100644 index 00000000..d74c9979 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift @@ -0,0 +1,26 @@ +// +// HumeAI.Client-Chats.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listChats() async throws -> [HumeAI.APISpecification.ResponseBodies.Chat] { + let response = try await run(\.listChats) + return response.chats + } + + public func listChatEvents(chatId: String) async throws -> [HumeAI.APISpecification.ResponseBodies.ChatEvent] { + let response = try await run(\.listChatEvents, with: chatId) + return response.events + } + + public func getChatAudio(chatId: String) async throws -> Data { + try await run(\.getChatAudio, with: chatId) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift new file mode 100644 index 00000000..a7a3d769 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift @@ -0,0 +1,34 @@ +// +// HumeAI.Client-Configs.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listConfigs() async throws -> [HumeAI.APISpecification.ResponseBodies.Config] { + let response = try await run(\.listConfigs) + return response.configs + } + + public func createConfig( + name: String, + description: String?, + settings: [String: String] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Config { + let input = HumeAI.APISpecification.RequestBodies.CreateConfigInput( + name: name, + description: description, + settings: settings + ) + return try await run(\.createConfig, with: input) + } + + public func deleteConfig(id: String) async throws { + try await run(\.deleteConfig, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift new file mode 100644 index 00000000..a27b698c --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift @@ -0,0 +1,34 @@ +// +// HumeAI.Client-CustomVoices.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listCustomVoices() async throws -> [HumeAI.APISpecification.ResponseBodies.Voice] { + let response = try await run(\.listCustomVoices) + return response.voices + } + + public func createCustomVoice( + name: String, + baseVoice: String, + parameters: HumeAI.APISpecification.ResponseBodies.VoiceParameters + ) async throws -> HumeAI.APISpecification.ResponseBodies.Voice { + let input = HumeAI.APISpecification.RequestBodies.CreateVoiceInput( + name: name, + baseVoice: baseVoice, + parameters: parameters + ) + return try await run(\.createCustomVoice, with: input) + } + + public func deleteCustomVoice(id: String) async throws { + try await run(\.deleteCustomVoice, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift new file mode 100644 index 00000000..800bbd19 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift @@ -0,0 +1,34 @@ +// +// HumeAI.Client-Datasets.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listDatasets() async throws -> [ResponseBodies.Dataset] { + let response = try await run(\.listDatasets) + return response.datasets + } + + public func createDataset( + name: String, + description: String?, + fileIds: [String] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Dataset { + let input = HumeAI.APISpecification.RequestBodies.CreateDatasetInput( + name: name, + description: description, + fileIds: fileIds + ) + return try await run(\.createDataset, with: input) + } + + public func deleteDataset(id: String) async throws { + try await run(\.deleteDataset, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift new file mode 100644 index 00000000..0106a016 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift @@ -0,0 +1,30 @@ +// +// HumeAI.Client-Files.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listFiles() async throws -> [HumeAI.APISpecification.ResponseBodies.File] { + let response = try await run(\.listFiles) + return response.files + } + + public func uploadFile( + data: Data, + name: String, + metadata: [String: String]? = nil + ) async throws -> HumeAI.APISpecification.ResponseBodies.File { + let input = HumeAI.APISpecification.RequestBodies.UploadFileInput(file: data, name: name, metadata: metadata) + return try await run(\.uploadFile, with: input) + } + + public func deleteFile(id: String) async throws { + try await run(\.deleteFile, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift new file mode 100644 index 00000000..67af5eae --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift @@ -0,0 +1,40 @@ +// +// HumeAI.Client-Jobs.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func startTrainingJob( + datasetId: String, + name: String, + description: String? = nil, + configuration: [String: String] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + let input = HumeAI.APISpecification.RequestBodies.TrainingJobInput( + datasetId: datasetId, + name: name, + description: description, + configuration: configuration + ) + return try await run(\.startTrainingJob, with: input) + } + + public func startCustomInferenceJob( + modelId: String, + files: [HumeAI.APISpecification.RequestBodies.CustomInferenceJobInput.FileInput], + configuration: [String: String] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + let input = HumeAI.APISpecification.RequestBodies.CustomInferenceJobInput( + modelId: modelId, + files: files, + configuration: configuration + ) + return try await run(\.startCustomInferenceJob, with: input) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift new file mode 100644 index 00000000..4ff09ace --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift @@ -0,0 +1,33 @@ +// +// HumeAI.Client-Models.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +//FIXME: - Not correct Model structure + +extension HumeAI.Client { + public func listModels() async throws -> [HumeAI.Model] { + let response = try await run(\.listModels) + return response.models + } + + public func getModel( + id: String + ) async throws -> HumeAI.Model { + try await run(\.getModel, with: id) + } + + public func updateModelName( + id: String, + name: String + ) async throws -> HumeAI.Model { + let input = HumeAI.APISpecification.RequestBodies.UpdateModelNameInput(name: name) + return try await run(\.updateModelName, with: input) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift new file mode 100644 index 00000000..e69df57f --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift @@ -0,0 +1,35 @@ +// +// Untitled.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listPrompts() async throws -> [HumeAI.APISpecification.ResponseBodies.Prompt] { + let response = try await run(\.listPrompts) + return response.prompts + } + + public func createPrompt( + name: String, + content: String, + description: String? = nil + ) async throws -> HumeAI.APISpecification.ResponseBodies.Prompt { + let input = HumeAI.APISpecification.RequestBodies.CreatePromptInput( + name: name, + description: description, + content: content, + metadata: nil + ) + return try await run(\.createPrompt, with: input) + } + + public func deletePrompt(id: String) async throws { + try await run(\.deletePrompt, with: id) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift new file mode 100644 index 00000000..7e826cd8 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift @@ -0,0 +1,21 @@ +// +// HumeAI.Client-Stream.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func streamInference( + file: Data, + models: [HumeAI.Model], + metadata: [String: String]? = nil + ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + let input = HumeAI.APISpecification.RequestBodies.StreamInput(file: file, models: models, metadata: metadata) + return try await run(\.streamInference, with: input) + } +} diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift new file mode 100644 index 00000000..43fca7e3 --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift @@ -0,0 +1,39 @@ +// +// HumeAI.Client-Tools.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import NetworkKit +import SwiftAPI +import Merge + +extension HumeAI.Client { + public func listTools() async throws -> [HumeAI.APISpecification.ResponseBodies.Tool] { + let response = try await run(\.listTools) + return response.tools + } + + public func createTool( + name: String, + description: String?, + configuration: [String: String] + ) async throws -> HumeAI.APISpecification.ResponseBodies.Tool { + let input = HumeAI.APISpecification.RequestBodies.CreateToolInput( + name: name, + description: description, + configuration: .init(parameters: configuration) + ) + return try await run(\.createTool, with: input) + } + + public func deleteTool(id: String) async throws { + try await run(\.deleteTool, with: id) + } + + public func updateToolName(id: String, name: String) async throws -> HumeAI.APISpecification.ResponseBodies.Tool { + let input = HumeAI.APISpecification.RequestBodies.UpdateToolNameInput(name: name) + return try await run(\.updateToolName, with: input) + } +} From 5b6787f7958207dbf58d10e16e2e55b91ec1c9ce Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 11:46:13 -0700 Subject: [PATCH 32/73] HumeAI --- ...umeAI.APISpecification.RequestBodies.swift | 120 +++++---- .../API/HumeAI.APISpecification.swift | 241 ++++++++++++------ ...eAI.APISpeicification.ResponseBodies.swift | 78 +----- .../Intramodular/HumeAI.Client-Batch.swift | 15 +- .../Intramodular/HumeAI.Client-Chat.swift | 6 +- .../HumeAI.Client-ChatGroups.swift | 26 +- .../Intramodular/HumeAI.Client-Chats.swift | 17 +- .../Intramodular/HumeAI.Client-Configs.swift | 11 +- .../HumeAI.Client-CustomVoices.swift | 13 +- .../Intramodular/HumeAI.Client-Datasets.swift | 9 +- .../Intramodular/HumeAI.Client-Files.swift | 15 +- .../Intramodular/HumeAI.Client-Jobs.swift | 6 +- .../Intramodular/HumeAI.Client-Models.swift | 10 +- .../Intramodular/HumeAI.Client-Prompts.swift | 9 +- .../Intramodular/HumeAI.Client-Stream.swift | 10 +- .../Intramodular/HumeAI.Client-Tools.swift | 25 +- .../HumeAI/Intramodular/HumeAI.Client.swift | 2 +- .../Intramodular/Models/HumeAI.Chat.swift | 15 ++ .../Models/HumeAI.ChatEvent.swift | 18 ++ .../Models/HumeAI.ChatGroup.swift | 16 ++ .../Models/HumeAI.ChatMessage.swift | 18 ++ .../Models/HumeAI.ChatResponse.swift | 32 +++ .../Intramodular/Models/HumeAI.Config.swift | 26 ++ .../Intramodular/Models/HumeAI.Dataset.swift | 26 ++ .../Intramodular/Models/HumeAI.File.swift | 18 ++ .../Models/HumeAI.FileInput.swift | 20 ++ .../Intramodular/Models/HumeAI.Job.swift | 33 +++ .../Intramodular/Models/HumeAI.Prompt.swift | 27 ++ .../Intramodular/Models/HumeAI.Tool.swift | 30 +++ 29 files changed, 637 insertions(+), 255 deletions(-) create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.File.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index 1d22ce42..066e2695 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -15,43 +15,24 @@ extension HumeAI.APISpecification { let pageNumber: Int? let pageSize: Int? let name: String? - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case name - } } struct CreateVoiceInput: Codable { let name: String let baseVoice: String let parameterModel: String - let parameters: Parameters? - - private enum CodingKeys: String, CodingKey { - case name - case baseVoice = "base_voice" - case parameterModel = "parameter_model" - case parameters - } - - struct Parameters: Codable { - let gender: Double? - let articulation: Double? - let assertiveness: Double? - let buoyancy: Double? - let confidence: Double? - let enthusiasm: Double? - let nasality: Double? - let relaxedness: Double? - let smoothness: Double? - let tepidity: Double? - let tightness: Double? - } + let parameters: HumeAI.Voice.Parameters? + } + + struct CreateVoiceVersionInput: Codable { + let id: String + let baseVoice: String + let parameterModel: String + let parameters: HumeAI.Voice.Parameters? } struct UpdateVoiceNameInput: Codable { + let id: String let name: String } @@ -74,23 +55,11 @@ extension HumeAI.APISpecification { } struct BatchInferenceJobInput: Codable { - let files: [FileInput] + let files: [HumeAI.FileInput] let models: [HumeAI.Model] let callback: CallbackConfig? } - struct FileInput: Codable { - let url: String - let mimeType: String - let metadata: [String: String]? - - private enum CodingKeys: String, CodingKey { - case url - case mimeType = "mime_type" - case metadata - } - } - struct CallbackConfig: Codable { let url: String let metadata: [String: String]? @@ -121,11 +90,20 @@ extension HumeAI.APISpecification { let settings: [String: String] } + struct CreateConfigVersionInput: Codable { + let id: String + let description: String? + let settings: [String: String] + } + struct UpdateConfigNameInput: Codable { + let id: String let name: String } struct UpdateConfigDescriptionInput: Codable { + let id: String + let versionID: String let description: String } @@ -133,11 +111,12 @@ extension HumeAI.APISpecification { let name: String let description: String? let fileIds: [String] - - private enum CodingKeys: String, CodingKey { - case name, description - case fileIds = "file_ids" - } + } + + struct CreateDatasetVersionInput: Codable { + let id: String + let description: String? + let fileIds: [String] } struct UploadFileInput: Codable, HTTPRequest.Multipart.ContentConvertible { @@ -159,7 +138,7 @@ extension HumeAI.APISpecification { result.append( .string( named: "metadata", - value: try JSONEncoder().encode(metadata).utf8String ?? "" + value: try JSONEncoder().encode(metadata).toUTF8String() ?? "" ) ) } @@ -168,6 +147,7 @@ extension HumeAI.APISpecification { } struct UpdateFileNameInput: Codable { + let id: String let name: String } @@ -176,29 +156,22 @@ extension HumeAI.APISpecification { let name: String let description: String? let configuration: [String: String] - - private enum CodingKeys: String, CodingKey { - case datasetId = "dataset_id" - case name, description, configuration - } } struct CustomInferenceJobInput: Codable { let modelId: String - let files: [FileInput] + let files: [HumeAI.FileInput] let configuration: [String: String] - - private enum CodingKeys: String, CodingKey { - case modelId = "model_id" - case files, configuration - } } struct UpdateModelNameInput: Codable { + let id: String let name: String } struct UpdateModelDescriptionInput: Codable { + let id: String + let versionId: String let description: String } @@ -209,15 +182,26 @@ extension HumeAI.APISpecification { let metadata: [String: String]? } + struct CreatePromptVersionInput: Codable { + let id: String + let description: String? + let content: String + let metadata: [String: String]? + } + struct UpdatePromptNameInput: Codable { + let id: String let name: String } struct UpdatePromptDescriptionInput: Codable { + let id: String + let versionID: String let description: String } struct StreamInput: Codable, HTTPRequest.Multipart.ContentConvertible { + let id: String // Add file ID let file: Data let models: [HumeAI.Model] let metadata: [String: String]? @@ -230,13 +214,13 @@ extension HumeAI.APISpecification { named: "file", data: file, filename: "file", - contentType: .binary + contentType: .json ) ) result.append( .string( named: "models", - value: try JSONEncoder().encode(models).utf8String ?? "" + value: try JSONEncoder().encode(models).toUTF8String() ?? "" ) ) @@ -244,7 +228,7 @@ extension HumeAI.APISpecification { result.append( .string( named: "metadata", - value: try JSONEncoder().encode(metadata).utf8String ?? "" + value: try JSONEncoder().encode(metadata).toUTF8String() ?? "" ) ) } @@ -254,6 +238,7 @@ extension HumeAI.APISpecification { } struct CreateToolInput: Codable { + let id: String let name: String let description: String? let configuration: Configuration @@ -263,11 +248,24 @@ extension HumeAI.APISpecification { } } + struct CreateToolVersionInput: Codable { + let id: String + let description: String? + let configuration: Configuration + + struct Configuration: Codable { + let parameters: [String: String] + } + } + struct UpdateToolNameInput: Codable { + let id: String let name: String } struct UpdateToolDescriptionInput: Codable { + let id: String + let versionID: String let description: String } } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index bc7496e9..4986c993 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -67,36 +67,50 @@ extension HumeAI { @POST @Path("/v0/tools") @Body(json: \.input) - var createTool = Endpoint() + var createTool = Endpoint() @GET - @Path({ context -> String in "/v0/tools/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)/versions" + }) var listToolVersions = Endpoint() @POST - @Path({ context -> String in "/v0/tools/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)/versions" + }) @Body(json: \.input) - var createToolVersion = Endpoint() + var createToolVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/tools/\(context.input.id)" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)" + }) var deleteTool = Endpoint() @PATCH - @Path({ context -> String in "/v0/tools/\(context.input.id)" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)" + }) @Body(json: \.input) - var updateToolName = Endpoint() + var updateToolName = Endpoint() @GET - @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" + }) var getToolVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" + }) var deleteToolVersion = Endpoint() @PATCH - @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/tools/\(context.input.id)/versions/\(context.input.versionID)" + }) @Body(json: \.input) var updateToolDescription = Endpoint() @@ -108,41 +122,52 @@ extension HumeAI { @POST @Path("/v0/prompts") @Body(json: \.input) - var createPrompt = Endpoint() + var createPrompt = Endpoint() @GET - @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions" }) - var listPromptVersions = Endpoint() + @Path({ context -> String in + "/v0/prompts/\(context.input.id)/versions" + }) + var listPromptVersions = Endpoint() @POST - @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/prompts/\(context.input.id)/versions" + }) @Body(json: \.input) - var createPromptVersion = Endpoint() + var createPromptVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/prompts/\(context.input.id)" }) + @Path({ context -> String in + "/v0/prompts/\(context.input.id)" + }) var deletePrompt = Endpoint() @PATCH - @Path( - { - context -> String in "/v0/prompts/\(context.input.id)" - }) + @Path({ context -> String in + "/v0/prompts/\(context.input.id)" + }) @Body(json: \.input) - var updatePromptName = Endpoint() + var updatePromptName = Endpoint() @GET - @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) - var getPromptVersion = Endpoint() + @Path({ context -> String in + "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" + }) + var getPromptVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" + }) var deletePromptVersion = Endpoint() @PATCH - @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/prompts/\(context.input.id)/versions/\(context.input.versionID)" + }) @Body(json: \.input) - var updatePromptDescription = Endpoint() + var updatePromptDescription = Endpoint() // MARK: - Custom Voices @GET @@ -155,20 +180,28 @@ extension HumeAI { var createCustomVoice = Endpoint() @GET - @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + @Path({ context -> String in + "/v0/custom-voices/\(context.input.id)" + }) var getCustomVoice = Endpoint() @POST - @Path({ context -> String in "/v0/custom-voices/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/custom-voices/\(context.input.id)/versions" + }) @Body(json: \.input) var createCustomVoiceVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + @Path({ context -> String in + "/v0/custom-voices/\(context.input.id)" + }) var deleteCustomVoice = Endpoint() @PATCH - @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) + @Path({ context -> String in + "/v0/custom-voices/\(context.input.id)" + }) @Body(json: \.input) var updateCustomVoiceName = Endpoint() @@ -180,36 +213,50 @@ extension HumeAI { @POST @Path("/v0/configs") @Body(json: \.input) - var createConfig = Endpoint() + var createConfig = Endpoint() @GET - @Path({ context -> String in "/v0/configs/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)/versions" + }) var listConfigVersions = Endpoint() @POST - @Path({ context -> String in "/v0/configs/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)/versions" + }) @Body(json: \.input) var createConfigVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/configs/\(context.input.id)" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)" + }) var deleteConfig = Endpoint() @PATCH - @Path({ context -> String in "/v0/configs/\(context.input.id)" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)" + }) @Body(json: \.input) - var updateConfigName = Endpoint() + var updateConfigName = Endpoint() @GET - @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" + }) var getConfigVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" + }) var deleteConfigVersion = Endpoint() @PATCH - @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/configs/\(context.input.id)/versions/\(context.input.versionID)" + }) @Body(json: \.input) var updateConfigDescription = Endpoint() @@ -219,11 +266,15 @@ extension HumeAI { var listChats = Endpoint() @GET - @Path({ context -> String in "/v0/chats/\(context.input.id)/events" }) + @Path({ context -> String in + "/v0/chats/\(context.input.id)/events" + }) var listChatEvents = Endpoint() @GET - @Path({ context -> String in "/v0/chats/\(context.input.id)/audio" }) + @Path({ context -> String in + "/v0/chats/\(context.input.id)/audio" + }) var getChatAudio = Endpoint() // MARK: - Chat Groups @@ -232,22 +283,28 @@ extension HumeAI { var listChatGroups = Endpoint() @GET - @Path({ context -> String in "/v0/chat-groups/\(context.input.id)" }) - var getChatGroup = Endpoint() + @Path({ context -> String in + "/v0/chat-groups/\(context.input.id)" + }) + var getChatGroup = Endpoint() @GET - @Path({ context -> String in "/v0/chat-groups/\(context.input.id)/events" }) + @Path({ context -> String in + "/v0/chat-groups/\(context.input.id)/events" + }) var listChatGroupEvents = Endpoint() @GET - @Path({ context -> String in "/v0/chat-groups/\(context.input.id)/audio" }) + @Path({ context -> String in + "/v0/chat-groups/\(context.input.id)/audio" + }) var getChatGroupAudio = Endpoint() // MARK: - Chat @POST @Path("/v0/chat") @Body(json: \.input) - var chat = Endpoint() + var chat = Endpoint() // MARK: - Batch @GET @@ -257,25 +314,31 @@ extension HumeAI { @POST @Path("/v0/batch/jobs") @Body(json: \.input) - var startInferenceJob = Endpoint() + var startInferenceJob = Endpoint() @GET - @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)" }) - var getJobDetails = Endpoint() + @Path({ context -> String in + "/v0/batch/jobs/\(context.input.id)" + }) + var getJobDetails = Endpoint() @GET - @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)/predictions" }) - var getJobPredictions = Endpoint() + @Path({ context -> String in + "/v0/batch/jobs/\(context.input.id)/predictions" + }) + var getJobPredictions = Endpoint() @GET - @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)/artifacts" }) + @Path({ context -> String in + "/v0/batch/jobs/\(context.input.id)/artifacts" + }) var getJobArtifacts = Endpoint() // MARK: - Stream @POST @Path("/v0/stream") @Body(multipart: .input) - var streamInference = Endpoint() + var streamInference = Endpoint() // MARK: - Files @GET @@ -285,24 +348,32 @@ extension HumeAI { @POST @Path("/v0/files") @Body(multipart: .input) - var uploadFile = Endpoint() + var uploadFile = Endpoint() @GET - @Path({ context -> String in "/v0/files/\(context.input.id)" }) - var getFile = Endpoint() + @Path({ context -> String in + "/v0/files/\(context.input.id)" + }) + var getFile = Endpoint() @DELETE - @Path({ context -> String in "/v0/files/\(context.input.id)" }) + @Path({ context -> String in + "/v0/files/\(context.input.id)" + }) var deleteFile = Endpoint() @PATCH - @Path({ context -> String in "/v0/files/\(context.input.id)" }) + @Path({ context -> String in + "/v0/files/\(context.input.id)" + }) @Body(json: \.input) - var updateFileName = Endpoint() + var updateFileName = Endpoint() @GET - @Path({ context -> String in "/v0/files/\(context.input.id)/predictions" }) - var getFilePredictions = Endpoint() + @Path({ context -> String in + "/v0/files/\(context.input.id)/predictions" + }) + var getFilePredictions = Endpoint() // MARK: - Datasets @GET @@ -312,23 +383,31 @@ extension HumeAI { @POST @Path("/v0/datasets") @Body(json: \.input) - var createDataset = Endpoint() + var createDataset = Endpoint() @GET - @Path({ context -> String in "/v0/datasets/\(context.input.id)" }) - var getDataset = Endpoint() + @Path({ context -> String in + "/v0/datasets/\(context.input.id)" + }) + var getDataset = Endpoint() @POST - @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/datasets/\(context.input.id)/versions" + }) @Body(json: \.input) var createDatasetVersion = Endpoint() @DELETE - @Path({ context -> String in "/v0/datasets/\(context.input.id)" }) + @Path({ context -> String in + "/v0/datasets/\(context.input.id)" + }) var deleteDataset = Endpoint() @GET - @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/datasets/\(context.input.id)/versions" + }) var listDatasetVersions = Endpoint() // MARK: - Models @GET @@ -336,24 +415,34 @@ extension HumeAI { var listModels = Endpoint() @GET - @Path({ context -> String in "/v0/models/\(context.input.id)" }) - var getModel = Endpoint() + @Path({ context -> String in + "/v0/models/\(context.input.id)" + }) + var getModel = Endpoint() @PATCH - @Path({ context -> String in "/v0/models/\(context.input.id)" }) + @Path({ context -> String in + "/v0/models/\(context.input.id)" + }) @Body(json: \.input) - var updateModelName = Endpoint() + var updateModelName = Endpoint() @GET - @Path({ context -> String in "/v0/models/\(context.input.id)/versions" }) + @Path({ context -> String in + "/v0/models/\(context.input.id)/versions" + }) var listModelVersions = Endpoint() @GET - @Path({ context -> String in "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" + }) var getModelVersion = Endpoint() @PATCH - @Path({ context -> String in "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" }) + @Path({ context -> String in + "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" + }) @Body(json: \.input) var updateModelDescription = Endpoint() @@ -361,12 +450,12 @@ extension HumeAI { @POST @Path("/v0/jobs/training") @Body(json: \.input) - var startTrainingJob = Endpoint() + var startTrainingJob = Endpoint() @POST @Path("/v0/jobs/inference") @Body(json: \.input) - var startCustomInferenceJob = Endpoint() + var startCustomInferenceJob = Endpoint() } } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index 997b36f6..a4ad60d3 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -132,7 +132,7 @@ extension HumeAI.APISpecification { let pageNumber: Int let pageSize: Int let totalPages: Int - let chats: [Chat] + let chats: [HumeAI.Chat] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" @@ -143,20 +143,11 @@ extension HumeAI.APISpecification { } struct ChatEventList: Codable { - let events: [ChatEvent] + let events: [HumeAI.ChatEvent] } typealias ChatAudio = Data - struct Config: Codable { - let id: String - let name: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let versions: [ConfigVersion]? - } - struct ConfigVersion: Codable { let id: String let configId: String @@ -170,7 +161,7 @@ extension HumeAI.APISpecification { let pageNumber: Int let pageSize: Int let totalPages: Int - let configs: [Config] + let configs: [HumeAI.Config] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" @@ -208,29 +199,11 @@ extension HumeAI.APISpecification { } } - struct Dataset: Codable { - let id: String - let name: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let versions: [DatasetVersion]? - } - - struct DatasetVersion: Codable { - let id: String - let datasetId: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let files: [File]? - } - struct DatasetList: Codable { let pageNumber: Int let pageSize: Int let totalPages: Int - let datasets: [Dataset] + let datasets: [HumeAI.Dataset] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" @@ -240,29 +213,11 @@ extension HumeAI.APISpecification { } } - struct File: Codable { - let id: String - let name: String - let size: Int - let mimeType: String - let createdOn: Int64 - let modifiedOn: Int64 - let metadata: [String: String]? - - private enum CodingKeys: String, CodingKey { - case id, name, size - case mimeType = "mime_type" - case createdOn = "created_on" - case modifiedOn = "modified_on" - case metadata - } - } - struct FileList: Codable { let pageNumber: Int let pageSize: Int let totalPages: Int - let files: [File] + let files: [HumeAI.File] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" @@ -304,30 +259,11 @@ extension HumeAI.APISpecification { } } - struct Prompt: Codable { - let id: String - let name: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let versions: [PromptVersion]? - } - - struct PromptVersion: Codable { - let id: String - let promptId: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let content: String - let metadata: [String: String]? - } - struct PromptList: Codable { let pageNumber: Int let pageSize: Int let totalPages: Int - let prompts: [Prompt] + let prompts: [HumeAI.Prompt] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" @@ -363,7 +299,7 @@ extension HumeAI.APISpecification { let pageNumber: Int let pageSize: Int let totalPages: Int - let tools: [Tool] + let tools: [HumeAI.Tool] private enum CodingKeys: String, CodingKey { case pageNumber = "page_number" diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift index e0ac8101..f8eba3b7 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift @@ -11,18 +11,23 @@ import Merge extension HumeAI.Client { public func startInferenceJob( - files: [HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput.FileInput], + files: [HumeAI.FileInput], models: [HumeAI.Model] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + ) async throws -> HumeAI.Job { let input = HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput( - files: files, + files: files.map { .init(url: $0.url, mimeType: $0.mimeType, metadata: $0.metadata) }, models: models, callback: nil ) return try await run(\.startInferenceJob, with: input) } - public func getJobDetails(id: String) async throws -> HumeAI.APISpecification.ResponseBodies.Job { - try await run(\.getJobDetails, with: id) + public func getJobDetails( + id: String + ) async throws -> HumeAI.Job { + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.getJobDetails, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift index dc011978..8907a823 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Chat.swift @@ -11,12 +11,12 @@ import Merge extension HumeAI.Client { public func chat( - messages: [HumeAI.APISpecification.RequestBodies.ChatRequest.Message], + messages: [HumeAI.ChatMessage], model: String, temperature: Double? = nil - ) async throws -> HumeAI.APISpecification.ResponseBodies.ChatResponse { + ) async throws -> HumeAI.ChatResponse { let input = HumeAI.APISpecification.RequestBodies.ChatRequest( - messages: messages, + messages: messages.map { .init(role: $0.role, content: $0.content) }, model: model, temperature: temperature, maxTokens: nil, diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift index c3683617..8951cd10 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-ChatGroups.swift @@ -10,12 +10,30 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listChatGroups() async throws -> [HumeAI.APISpecification.ResponseBodies.ChatGroup] { + public func listChatGroups() async throws -> [HumeAI.ChatGroup] { let response = try await run(\.listChatGroups) - return response.chatGroups + return response.chatGroups.map { chatGroup in + HumeAI.ChatGroup( + id: chatGroup.id, + name: chatGroup.name, + createdOn: chatGroup.createdOn, + modifiedOn: chatGroup.modifiedOn, + chats: chatGroup.chats?.compactMap { chat in + HumeAI.Chat( + id: chat.id, + name: chat.name, + createdOn: chat.createdOn, + modifiedOn: chat.modifiedOn + ) + } + ) + } } - public func getChatGroup(id: String) async throws -> HumeAI.APISpecification.ResponseBodies.ChatGroup { - try await run(\.getChatGroup, with: id) + public func getChatGroup(id: String) async throws -> HumeAI.ChatGroup { + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.getChatGroup, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift index d74c9979..d78e7efa 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Chats.swift @@ -10,17 +10,26 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listChats() async throws -> [HumeAI.APISpecification.ResponseBodies.Chat] { + public func listChats() async throws -> [HumeAI.Chat] { let response = try await run(\.listChats) + return response.chats } - public func listChatEvents(chatId: String) async throws -> [HumeAI.APISpecification.ResponseBodies.ChatEvent] { - let response = try await run(\.listChatEvents, with: chatId) + public func listChatEvents(chatId: String) async throws -> [HumeAI.ChatEvent] { + let input = HumeAI.APISpecification.PathInput.ID( + id: chatId + ) + let response = try await run(\.listChatEvents, with: input) + return response.events } public func getChatAudio(chatId: String) async throws -> Data { - try await run(\.getChatAudio, with: chatId) + let input = HumeAI.APISpecification.PathInput.ID( + id: chatId + ) + + return try await run(\.getChatAudio, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift index a7a3d769..2bf56697 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Configs.swift @@ -10,8 +10,9 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listConfigs() async throws -> [HumeAI.APISpecification.ResponseBodies.Config] { + public func listConfigs() async throws -> [HumeAI.Config] { let response = try await run(\.listConfigs) + return response.configs } @@ -19,7 +20,7 @@ extension HumeAI.Client { name: String, description: String?, settings: [String: String] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Config { + ) async throws -> HumeAI.Config { let input = HumeAI.APISpecification.RequestBodies.CreateConfigInput( name: name, description: description, @@ -29,6 +30,10 @@ extension HumeAI.Client { } public func deleteConfig(id: String) async throws { - try await run(\.deleteConfig, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + + try await run(\.deleteConfig, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift index a27b698c..8751e6c7 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift @@ -10,7 +10,7 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listCustomVoices() async throws -> [HumeAI.APISpecification.ResponseBodies.Voice] { + public func listCustomVoices() async throws -> [HumeAI.Voice] { let response = try await run(\.listCustomVoices) return response.voices } @@ -18,17 +18,22 @@ extension HumeAI.Client { public func createCustomVoice( name: String, baseVoice: String, - parameters: HumeAI.APISpecification.ResponseBodies.VoiceParameters - ) async throws -> HumeAI.APISpecification.ResponseBodies.Voice { + model: HumeAI.Model, + parameters: HumeAI.Voice.Parameters + ) async throws -> HumeAI.Voice { let input = HumeAI.APISpecification.RequestBodies.CreateVoiceInput( name: name, baseVoice: baseVoice, + parameterModel: model.rawValue, parameters: parameters ) return try await run(\.createCustomVoice, with: input) } public func deleteCustomVoice(id: String) async throws { - try await run(\.deleteCustomVoice, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + try await run(\.deleteCustomVoice, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift index 800bbd19..9076da25 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Datasets.swift @@ -10,7 +10,7 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listDatasets() async throws -> [ResponseBodies.Dataset] { + public func listDatasets() async throws -> [HumeAI.Dataset] { let response = try await run(\.listDatasets) return response.datasets } @@ -19,7 +19,7 @@ extension HumeAI.Client { name: String, description: String?, fileIds: [String] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Dataset { + ) async throws -> HumeAI.Dataset { let input = HumeAI.APISpecification.RequestBodies.CreateDatasetInput( name: name, description: description, @@ -29,6 +29,9 @@ extension HumeAI.Client { } public func deleteDataset(id: String) async throws { - try await run(\.deleteDataset, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + try await run(\.deleteDataset, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift index 0106a016..54184c71 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Files.swift @@ -10,7 +10,7 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listFiles() async throws -> [HumeAI.APISpecification.ResponseBodies.File] { + public func listFiles() async throws -> [HumeAI.File] { let response = try await run(\.listFiles) return response.files } @@ -19,12 +19,19 @@ extension HumeAI.Client { data: Data, name: String, metadata: [String: String]? = nil - ) async throws -> HumeAI.APISpecification.ResponseBodies.File { - let input = HumeAI.APISpecification.RequestBodies.UploadFileInput(file: data, name: name, metadata: metadata) + ) async throws -> HumeAI.File { + let input = HumeAI.APISpecification.RequestBodies.UploadFileInput( + file: data, + name: name, + metadata: metadata + ) return try await run(\.uploadFile, with: input) } public func deleteFile(id: String) async throws { - try await run(\.deleteFile, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + try await run(\.deleteFile, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift index 67af5eae..c96f9527 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift @@ -15,7 +15,7 @@ extension HumeAI.Client { name: String, description: String? = nil, configuration: [String: String] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + ) async throws -> HumeAI.Job { let input = HumeAI.APISpecification.RequestBodies.TrainingJobInput( datasetId: datasetId, name: name, @@ -27,9 +27,9 @@ extension HumeAI.Client { public func startCustomInferenceJob( modelId: String, - files: [HumeAI.APISpecification.RequestBodies.CustomInferenceJobInput.FileInput], + files: [HumeAI.FileInput], configuration: [String: String] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { + ) async throws -> HumeAI.Job { let input = HumeAI.APISpecification.RequestBodies.CustomInferenceJobInput( modelId: modelId, files: files, diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift index 4ff09ace..e34714ec 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift @@ -20,14 +20,20 @@ extension HumeAI.Client { public func getModel( id: String ) async throws -> HumeAI.Model { - try await run(\.getModel, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.getModel, with: input) } public func updateModelName( id: String, name: String ) async throws -> HumeAI.Model { - let input = HumeAI.APISpecification.RequestBodies.UpdateModelNameInput(name: name) + let input = HumeAI.APISpecification.RequestBodies.UpdateModelNameInput( + id: id, + name: name + ) return try await run(\.updateModelName, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift index e69df57f..77c742ae 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift @@ -10,7 +10,7 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listPrompts() async throws -> [HumeAI.APISpecification.ResponseBodies.Prompt] { + public func listPrompts() async throws -> [HumeAI.Prompt] { let response = try await run(\.listPrompts) return response.prompts } @@ -19,7 +19,7 @@ extension HumeAI.Client { name: String, content: String, description: String? = nil - ) async throws -> HumeAI.APISpecification.ResponseBodies.Prompt { + ) async throws -> HumeAI.Prompt { let input = HumeAI.APISpecification.RequestBodies.CreatePromptInput( name: name, description: description, @@ -30,6 +30,9 @@ extension HumeAI.Client { } public func deletePrompt(id: String) async throws { - try await run(\.deletePrompt, with: id) + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + try await run(\.deletePrompt, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift index 7e826cd8..6eddd27c 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift @@ -11,11 +11,17 @@ import Merge extension HumeAI.Client { public func streamInference( + id: String, file: Data, models: [HumeAI.Model], metadata: [String: String]? = nil - ) async throws -> HumeAI.APISpecification.ResponseBodies.Job { - let input = HumeAI.APISpecification.RequestBodies.StreamInput(file: file, models: models, metadata: metadata) + ) async throws -> HumeAI.Job { + let input = HumeAI.APISpecification.RequestBodies.StreamInput( + id: id, + file: file, + models: models, + metadata: metadata + ) return try await run(\.streamInference, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift index 43fca7e3..c09e9b36 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift @@ -10,17 +10,19 @@ import SwiftAPI import Merge extension HumeAI.Client { - public func listTools() async throws -> [HumeAI.APISpecification.ResponseBodies.Tool] { + public func listTools() async throws -> [HumeAI.Tool] { let response = try await run(\.listTools) return response.tools } public func createTool( + id: String, name: String, description: String?, configuration: [String: String] - ) async throws -> HumeAI.APISpecification.ResponseBodies.Tool { + ) async throws -> HumeAI.Tool { let input = HumeAI.APISpecification.RequestBodies.CreateToolInput( + id: id, name: name, description: description, configuration: .init(parameters: configuration) @@ -28,12 +30,23 @@ extension HumeAI.Client { return try await run(\.createTool, with: input) } - public func deleteTool(id: String) async throws { - try await run(\.deleteTool, with: id) + public func deleteTool( + id: String + ) async throws { + + let input = HumeAI.APISpecification.PathInput.ID(id: id) + + try await run(\.deleteTool, with: input) } - public func updateToolName(id: String, name: String) async throws -> HumeAI.APISpecification.ResponseBodies.Tool { - let input = HumeAI.APISpecification.RequestBodies.UpdateToolNameInput(name: name) + public func updateToolName( + id: String, + name: String + ) async throws -> HumeAI.Tool { + let input = HumeAI.APISpecification.RequestBodies.UpdateToolNameInput( + id: id, + name: name + ) return try await run(\.updateToolName, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client.swift b/Sources/HumeAI/Intramodular/HumeAI.Client.swift index 2adafb8f..5471ef16 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client.swift @@ -44,7 +44,7 @@ extension HumeAI { extension HumeAI.Client { // Text to Speech public func getAllAvailableVoices() async throws -> [HumeAI.Voice] { - let response = try await run(\.listVoices) + let response = try await run(\.listCustomVoices) return response.voices } } diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift new file mode 100644 index 00000000..8259acd0 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift @@ -0,0 +1,15 @@ +// +// HumeAI.Chat.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Chat: Codable { + public let id: String + public let name: String + public let createdOn: Int64 + public let modifiedOn: Int64 + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift new file mode 100644 index 00000000..3d17547d --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift @@ -0,0 +1,18 @@ +// +// HumeAI.ChatEvent.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct ChatEvent: Codable { + public let id: String + public let chatId: String + public let type: String + public let content: String + public let createdOn: Int64 + public let audioUrl: String? + public let metadata: [String: String]? + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift new file mode 100644 index 00000000..3037b590 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift @@ -0,0 +1,16 @@ +// +// HumeAI.ChatGroup.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct ChatGroup { + public let id: String + public let name: String + public let createdOn: Int64 + public let modifiedOn: Int64 + public let chats: [Chat]? + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift new file mode 100644 index 00000000..1085fbbb --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift @@ -0,0 +1,18 @@ +// +// HumeAI.ChatMessage.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct ChatMessage { + public let role: String + public let content: String + + public init(role: String, content: String) { + self.role = role + self.content = content + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift new file mode 100644 index 00000000..6353d5b2 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift @@ -0,0 +1,32 @@ +// +// HumeAI.ChatResponse.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct ChatResponse { + public let id: String + public let created: Int64 + public let choices: [Choice] + public let usage: Usage + + public struct Choice { + public let index: Int + public let message: Message + public let finishReason: String? + + public struct Message { + public let role: String + public let content: String + } + } + + public struct Usage { + public let promptTokens: Int + public let completionTokens: Int + public let totalTokens: Int + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift new file mode 100644 index 00000000..6cd8d926 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift @@ -0,0 +1,26 @@ +// +// HumeAI.Config.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Config: Codable { + public let id: String + public let name: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let versions: [ConfigVersion]? + + public struct ConfigVersion: Codable { + public let id: String + public let configId: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let settings: [String: String] + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift new file mode 100644 index 00000000..ab06b510 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift @@ -0,0 +1,26 @@ +// +// HumeAI.Dataset.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Dataset: Codable { + public let id: String + public let name: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let versions: [DatasetVersion]? + + public struct DatasetVersion: Codable { + public let id: String + public let datasetId: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let files: [HumeAI.File]? + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift new file mode 100644 index 00000000..549e6589 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift @@ -0,0 +1,18 @@ +// +// HumeAI.File.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct File: Codable { + public let id: String + public let name: String + public let size: Int + public let mimeType: String + public let createdOn: Int64 + public let modifiedOn: Int64 + public let metadata: [String: String]? + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift new file mode 100644 index 00000000..b3d57c99 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift @@ -0,0 +1,20 @@ +// +// HumeAI.FileInput.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct FileInput: Codable { + public let url: String + public let mimeType: String + public let metadata: [String: String]? + + public init(url: String, mimeType: String, metadata: [String: String]? = nil) { + self.url = url + self.mimeType = mimeType + self.metadata = metadata + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift new file mode 100644 index 00000000..5617d7bd --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift @@ -0,0 +1,33 @@ +// +// HumeAI.Job.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Job { + public let id: String + public let status: String + public let createdOn: Int64 + public let modifiedOn: Int64 + public let predictions: [Prediction]? + public let artifacts: [String: String]? + + public struct Prediction { + public let file: FileInfo + public let results: [ModelResult] + + public struct FileInfo { + public let url: String + public let mimeType: String + public let metadata: [String: String]? + } + + public struct ModelResult { + public let model: String + public let results: [String: String] + } + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift new file mode 100644 index 00000000..362e4852 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift @@ -0,0 +1,27 @@ +// +// HumeAI.Prompt.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Prompt: Codable { + public let id: String + public let name: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let versions: [PromptVersion]? + + public struct PromptVersion: Codable { + public let id: String + public let promptId: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let content: String + public let metadata: [String: String]? + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift new file mode 100644 index 00000000..aa632f7c --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift @@ -0,0 +1,30 @@ +// +// HumeAI.Tool.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Tool: Codable { + public let id: String + public let name: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let versions: [ToolVersion]? + + public struct ToolVersion: Codable { + public let id: String + public let toolId: String + public let description: String? + public let createdOn: Int64 + public let modifiedOn: Int64 + public let configuration: Configuration + + public struct Configuration: Codable { + public let parameters: [String: String] + } + } + } +} From 73db59937d80ad86ae4f5ad18b0c9439d863efb7 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 11:52:50 -0700 Subject: [PATCH 33/73] Build succeeds --- Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 4986c993..625c6c8d 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -396,7 +396,7 @@ extension HumeAI { "/v0/datasets/\(context.input.id)/versions" }) @Body(json: \.input) - var createDatasetVersion = Endpoint() + var createDatasetVersion = Endpoint() @DELETE @Path({ context -> String in @@ -408,7 +408,7 @@ extension HumeAI { @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) - var listDatasetVersions = Endpoint() + var listDatasetVersions = Endpoint() // MARK: - Models @GET @Path("/v0/models") From b850efdb90289af84773df9eca1df770370e4fdb Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 12:16:35 -0700 Subject: [PATCH 34/73] tests start --- .../API/HumeAI.APISpecification.swift | 50 +++--- Tests/HumeAI/Intramodular/Batch.swift | 47 +++++ Tests/HumeAI/Intramodular/Chat.swift | 32 ++++ Tests/HumeAI/Intramodular/ChatGroup.swift | 32 ++++ Tests/HumeAI/Intramodular/Config.swift | 56 ++++++ Tests/HumeAI/Intramodular/Dataset.swift | 56 ++++++ Tests/HumeAI/Intramodular/File.swift | 41 +++++ Tests/HumeAI/Intramodular/Job.swift | 168 ++++++++++++++++++ Tests/HumeAI/Intramodular/Model.swift | 42 +++++ Tests/HumeAI/Intramodular/Prompts.swift | 56 ++++++ Tests/HumeAI/Intramodular/Stream.swift | 21 +++ Tests/HumeAI/Intramodular/Tests.swift | 7 - Tests/HumeAI/Intramodular/Tools.swift | 56 ++++++ Tests/HumeAI/Intramodular/Voices.swift | 46 +++++ Tests/HumeAI/module.swift | 7 + 15 files changed, 685 insertions(+), 32 deletions(-) create mode 100644 Tests/HumeAI/Intramodular/Batch.swift create mode 100644 Tests/HumeAI/Intramodular/Chat.swift create mode 100644 Tests/HumeAI/Intramodular/ChatGroup.swift create mode 100644 Tests/HumeAI/Intramodular/Config.swift create mode 100644 Tests/HumeAI/Intramodular/Dataset.swift create mode 100644 Tests/HumeAI/Intramodular/File.swift create mode 100644 Tests/HumeAI/Intramodular/Job.swift create mode 100644 Tests/HumeAI/Intramodular/Model.swift create mode 100644 Tests/HumeAI/Intramodular/Prompts.swift create mode 100644 Tests/HumeAI/Intramodular/Stream.swift delete mode 100644 Tests/HumeAI/Intramodular/Tests.swift create mode 100644 Tests/HumeAI/Intramodular/Tools.swift create mode 100644 Tests/HumeAI/Intramodular/Voices.swift diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 625c6c8d..9890841c 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -66,7 +66,7 @@ extension HumeAI { @POST @Path("/v0/tools") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createTool = Endpoint() @GET @@ -79,7 +79,7 @@ extension HumeAI { @Path({ context -> String in "/v0/tools/\(context.input.id)/versions" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createToolVersion = Endpoint() @DELETE @@ -92,7 +92,7 @@ extension HumeAI { @Path({ context -> String in "/v0/tools/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateToolName = Endpoint() @GET @@ -111,7 +111,7 @@ extension HumeAI { @Path({ context -> String in "/v0/tools/\(context.input.id)/versions/\(context.input.versionID)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateToolDescription = Endpoint() // MARK: - Prompts @@ -121,7 +121,7 @@ extension HumeAI { @POST @Path("/v0/prompts") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createPrompt = Endpoint() @GET @@ -134,7 +134,7 @@ extension HumeAI { @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createPromptVersion = Endpoint() @DELETE @@ -147,7 +147,7 @@ extension HumeAI { @Path({ context -> String in "/v0/prompts/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updatePromptName = Endpoint() @GET @@ -166,7 +166,7 @@ extension HumeAI { @Path({ context -> String in "/v0/prompts/\(context.input.id)/versions/\(context.input.versionID)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updatePromptDescription = Endpoint() // MARK: - Custom Voices @@ -176,7 +176,7 @@ extension HumeAI { @POST @Path("/v0/custom-voices") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createCustomVoice = Endpoint() @GET @@ -189,7 +189,7 @@ extension HumeAI { @Path({ context -> String in "/v0/custom-voices/\(context.input.id)/versions" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createCustomVoiceVersion = Endpoint() @DELETE @@ -202,7 +202,7 @@ extension HumeAI { @Path({ context -> String in "/v0/custom-voices/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateCustomVoiceName = Endpoint() // MARK: - Configs @@ -212,7 +212,7 @@ extension HumeAI { @POST @Path("/v0/configs") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createConfig = Endpoint() @GET @@ -225,7 +225,7 @@ extension HumeAI { @Path({ context -> String in "/v0/configs/\(context.input.id)/versions" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createConfigVersion = Endpoint() @DELETE @@ -238,7 +238,7 @@ extension HumeAI { @Path({ context -> String in "/v0/configs/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateConfigName = Endpoint() @GET @@ -257,7 +257,7 @@ extension HumeAI { @Path({ context -> String in "/v0/configs/\(context.input.id)/versions/\(context.input.versionID)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateConfigDescription = Endpoint() // MARK: - Chats @@ -303,7 +303,7 @@ extension HumeAI { // MARK: - Chat @POST @Path("/v0/chat") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var chat = Endpoint() // MARK: - Batch @@ -313,7 +313,7 @@ extension HumeAI { @POST @Path("/v0/batch/jobs") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var startInferenceJob = Endpoint() @GET @@ -366,7 +366,7 @@ extension HumeAI { @Path({ context -> String in "/v0/files/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateFileName = Endpoint() @GET @@ -382,7 +382,7 @@ extension HumeAI { @POST @Path("/v0/datasets") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createDataset = Endpoint() @GET @@ -395,7 +395,7 @@ extension HumeAI { @Path({ context -> String in "/v0/datasets/\(context.input.id)/versions" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createDatasetVersion = Endpoint() @DELETE @@ -424,7 +424,7 @@ extension HumeAI { @Path({ context -> String in "/v0/models/\(context.input.id)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateModelName = Endpoint() @GET @@ -443,18 +443,18 @@ extension HumeAI { @Path({ context -> String in "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" }) - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateModelDescription = Endpoint() // MARK: - Jobs @POST @Path("/v0/jobs/training") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var startTrainingJob = Endpoint() @POST @Path("/v0/jobs/inference") - @Body(json: \.input) + @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var startCustomInferenceJob = Endpoint() } } @@ -487,7 +487,7 @@ extension HumeAI.APISpecification { request = request .header("Accept", "application/json") - .header(.authorization(.bearer, apiKey)) + .header("X-Hume-Api-Key", apiKey) .header(.contentType(.json)) return request diff --git a/Tests/HumeAI/Intramodular/Batch.swift b/Tests/HumeAI/Intramodular/Batch.swift new file mode 100644 index 00000000..c241591f --- /dev/null +++ b/Tests/HumeAI/Intramodular/Batch.swift @@ -0,0 +1,47 @@ +// +// Batch.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientBatchTests: XCTestCase { + func testStartInferenceJob() async throws { + let job = try await client.startInferenceJob( + files: [.init( + url: "test-url", + mimeType: "test/mime" + )], + models: [.burst] + ) + XCTAssertNotNil(job) + } + + func testGetJobDetails() async throws { + let job = try await client.getJobDetails(id: "test-id") + XCTAssertNotNil(job) + } + + func testListJobs() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetJobPredictions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetJobArtifacts() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testStartInferenceJobFromLocalFile() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Chat.swift b/Tests/HumeAI/Intramodular/Chat.swift new file mode 100644 index 00000000..7708d3c2 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Chat.swift @@ -0,0 +1,32 @@ +// +// Chat.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientChatTests: XCTestCase { + + func testListChats() async throws { + let chats = try await client.listChats() + XCTAssertNotNil(chats) + } + + func testListChatEvents() async throws { + let events = try await client.listChatEvents(chatId: "test-id") + XCTAssertNotNil(events) + } + + func testGetChatAudio() async throws { + let audio = try await client.getChatAudio(chatId: "test-id") + XCTAssertNotNil(audio) + } + + func testChat() async throws { + let response = try await client.chat(messages: [.init(role: "user", content: "Hello")], model: "test-model") + XCTAssertNotNil(response) + } +} diff --git a/Tests/HumeAI/Intramodular/ChatGroup.swift b/Tests/HumeAI/Intramodular/ChatGroup.swift new file mode 100644 index 00000000..b352eb9e --- /dev/null +++ b/Tests/HumeAI/Intramodular/ChatGroup.swift @@ -0,0 +1,32 @@ +// +// ChatGroup.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientChatGroupTests: XCTestCase { + + func testListChatGroups() async throws { + let groups = try await client.listChatGroups() + XCTAssertNotNil(groups) + } + + func testGetChatGroup() async throws { + let group = try await client.getChatGroup(id: "test-id") + XCTAssertNotNil(group) + } + + func testListChatGroupEvents() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetChatGroupAudio() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Config.swift b/Tests/HumeAI/Intramodular/Config.swift new file mode 100644 index 00000000..979e2b99 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Config.swift @@ -0,0 +1,56 @@ +// +// Config.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientConfigTests: XCTestCase { + + func testListConfigs() async throws { + let configs = try await client.listConfigs() + XCTAssertNotNil(configs) + } + + func testCreateConfig() async throws { + let config = try await client.createConfig(name: "Test Config", description: "Test Description", settings: ["key": "value"]) + XCTAssertEqual(config.name, "Test Config") + } + + func testDeleteConfig() async throws { + try await client.deleteConfig(id: "test-id") + } + + func testListConfigVersions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testCreateConfigVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetConfigVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testDeleteConfigVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateConfigName() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateConfigDescription() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Dataset.swift b/Tests/HumeAI/Intramodular/Dataset.swift new file mode 100644 index 00000000..e401de16 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Dataset.swift @@ -0,0 +1,56 @@ +// +// Dataset.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientDatasetTests: XCTestCase { + + func testListDatasets() async throws { + let datasets = try await client.listDatasets() + XCTAssertNotNil(datasets) + } + + func testCreateDataset() async throws { + let dataset = try await client.createDataset(name: "Test Dataset", description: "Test Description", fileIds: ["test-id"]) + XCTAssertNotNil(dataset) + } + + func testDeleteDataset() async throws { + try await client.deleteDataset(id: "test-id") + } + + func testGetDataset() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testCreateDatasetVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testListDatasetVersions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testListDatasetFiles() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetDatasetVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testListDatasetVersionFiles() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/File.swift b/Tests/HumeAI/Intramodular/File.swift new file mode 100644 index 00000000..5a32fe02 --- /dev/null +++ b/Tests/HumeAI/Intramodular/File.swift @@ -0,0 +1,41 @@ +// +// File.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientFileTests: XCTestCase { + + func testListFiles() async throws { + let files = try await client.listFiles() + XCTAssertNotNil(files) + } + + func testUploadFile() async throws { + let file = try await client.uploadFile(data: Data(), name: "test.txt") + XCTAssertNotNil(file) + } + + func testDeleteFile() async throws { + try await client.deleteFile(id: "test-id") + } + + func testGetFile() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateFileName() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetFilePredictions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Job.swift b/Tests/HumeAI/Intramodular/Job.swift new file mode 100644 index 00000000..39464877 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Job.swift @@ -0,0 +1,168 @@ +// +// Job.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientJobTests: XCTestCase { + + // Training Jobs + func testStartTrainingJob() async throws { + let job = try await client.startTrainingJob( + datasetId: "test-id", + name: "Test Training Job", + description: "Test Description", + configuration: ["key": "value"] + ) + XCTAssertNotNil(job) + XCTAssertEqual(job.status, "pending") // Assuming initial status is pending + XCTAssertNotNil(job.id) + } + + func testStartTrainingJobWithoutDescription() async throws { + let job = try await client.startTrainingJob( + datasetId: "test-id", + name: "Test Training Job", + configuration: ["key": "value"] + ) + XCTAssertNotNil(job) + } + + // Custom Inference Jobs + func testStartCustomInferenceJob() async throws { + let files = [ + HumeAI.FileInput( + url: "test-url", + mimeType: "test/mime", + metadata: ["key": "value"] + ) + ] + + let job = try await client.startCustomInferenceJob( + modelId: "test-id", + files: files, + configuration: ["key": "value"] + ) + XCTAssertNotNil(job) + XCTAssertNotNil(job.id) + } + + // Job Status and Progress + func testGetJobDetails() async throws { + let job = try await client.getJobDetails(id: "test-id") + XCTAssertNotNil(job) + XCTAssertNotNil(job.status) + XCTAssertNotNil(job.createdOn) + XCTAssertNotNil(job.modifiedOn) + } + + // Error Cases + func testStartTrainingJobWithInvalidDataset() async throws { + do { + _ = try await client.startTrainingJob( + datasetId: "invalid-id", + name: "Test Job", + configuration: ["key": "value"] + ) + XCTFail("Expected error for invalid dataset ID") + } catch { + // Expected error + XCTAssertNotNil(error) + } + } + + func testStartCustomInferenceJobWithInvalidModel() async throws { + do { + _ = try await client.startCustomInferenceJob( + modelId: "invalid-id", + files: [.init(url: "test-url", mimeType: "test/mime")], + configuration: ["key": "value"] + ) + XCTFail("Expected error for invalid model ID") + } catch { + // Expected error + XCTAssertNotNil(error) + } + } + + func testGetInvalidJobDetails() async throws { + do { + _ = try await client.getJobDetails(id: "invalid-id") + XCTFail("Expected error for invalid job ID") + } catch { + // Expected error + XCTAssertNotNil(error) + } + } + + // Job Results + func testJobPredictions() async throws { + let job = try await client.getJobDetails(id: "test-id") + + if let predictions = job.predictions { + for prediction in predictions { + // Validate file info + XCTAssertNotNil(prediction.file.url) + XCTAssertNotNil(prediction.file.mimeType) + + // Validate results + XCTAssertFalse(prediction.results.isEmpty) + for result in prediction.results { + XCTAssertNotNil(result.model) + XCTAssertNotNil(result.results) + } + } + } + } + + // Job Artifacts + func testJobArtifacts() async throws { + let job = try await client.getJobDetails(id: "test-id") + + if let artifacts = job.artifacts { + XCTAssertFalse(artifacts.isEmpty) + } + } + + // Unimplemented Methods Tests + func testListJobs() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetJobPredictions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetJobArtifacts() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testStartInferenceJobFromLocalFile() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + // Job Status Transitions + func testJobStatusTransitions() async throws { + let job = try await client.getJobDetails(id: "test-id") + + // Verify status is a valid value + let validStatuses = ["pending", "running", "completed", "failed"] + XCTAssertTrue(validStatuses.contains(job.status)) + } + + // Validation Tests + func testJobTimestamps() async throws { + let job = try await client.getJobDetails(id: "test-id") + + // Created timestamp should be before or equal to modified timestamp + XCTAssertLessThanOrEqual(job.createdOn, job.modifiedOn) + } +} diff --git a/Tests/HumeAI/Intramodular/Model.swift b/Tests/HumeAI/Intramodular/Model.swift new file mode 100644 index 00000000..81cbca75 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Model.swift @@ -0,0 +1,42 @@ +// +// Model.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientModelTests: XCTestCase { + + func testListModels() async throws { + let models = try await client.listModels() + XCTAssertNotNil(models) + } + + func testGetModel() async throws { + let model = try await client.getModel(id: "test-id") + XCTAssertNotNil(model) + } + + func testUpdateModelName() async throws { + let model = try await client.updateModelName(id: "test-id", name: "Updated Name") + XCTAssertNotNil(model) + } + + func testListModelVersions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetModelVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateModelDescription() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Prompts.swift b/Tests/HumeAI/Intramodular/Prompts.swift new file mode 100644 index 00000000..abac71ce --- /dev/null +++ b/Tests/HumeAI/Intramodular/Prompts.swift @@ -0,0 +1,56 @@ +// +// Prompts.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientPromptTests: XCTestCase { + + func testListPrompts() async throws { + let prompts = try await client.listPrompts() + XCTAssertNotNil(prompts) + } + + func testCreatePrompt() async throws { + let prompt = try await client.createPrompt(name: "Test Prompt", content: "Test Content", description: "Test Description") + XCTAssertEqual(prompt.name, "Test Prompt") + } + + func testDeletePrompt() async throws { + try await client.deletePrompt(id: "test-id") + } + + func testListPromptVersions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testCreatePromptVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testGetPromptVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testDeletePromptVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdatePromptName() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdatePromptDescription() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Stream.swift b/Tests/HumeAI/Intramodular/Stream.swift new file mode 100644 index 00000000..b27e4539 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Stream.swift @@ -0,0 +1,21 @@ +// +// Stream.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientStreamTests: XCTestCase { + + func testStreamInference() async throws { + let job = try await client.streamInference( + id: "test-id", + file: Data(), + models: [.language] + ) + XCTAssertNotNil(job) + } +} diff --git a/Tests/HumeAI/Intramodular/Tests.swift b/Tests/HumeAI/Intramodular/Tests.swift deleted file mode 100644 index f09e84e7..00000000 --- a/Tests/HumeAI/Intramodular/Tests.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// Tests.swift -// AI -// -// Created by Jared Davidson on 11/22/24. -// - diff --git a/Tests/HumeAI/Intramodular/Tools.swift b/Tests/HumeAI/Intramodular/Tools.swift new file mode 100644 index 00000000..e888c2e5 --- /dev/null +++ b/Tests/HumeAI/Intramodular/Tools.swift @@ -0,0 +1,56 @@ +// +// Tools.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientToolTests: XCTestCase { + + func testListTools() async throws { + let tools = try await client.listTools() + XCTAssertNotNil(tools) + } + + func testCreateTool() async throws { + let tool = try await client.createTool(id: "test-id", name: "Test Tool", description: "Test Description", configuration: ["key": "value"]) + XCTAssertEqual(tool.name, "Test Tool") + } + + func testListToolVersions() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testCreateToolVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testDeleteTool() async throws { + try await client.deleteTool(id: "test-id") + } + + func testUpdateToolName() async throws { + let tool = try await client.updateToolName(id: "test-id", name: "Updated Name") + XCTAssertEqual(tool.name, "Updated Name") + } + + func testGetToolVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testDeleteToolVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateToolDescription() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/Intramodular/Voices.swift b/Tests/HumeAI/Intramodular/Voices.swift new file mode 100644 index 00000000..6ac53a3e --- /dev/null +++ b/Tests/HumeAI/Intramodular/Voices.swift @@ -0,0 +1,46 @@ +// +// Voices.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import XCTest +@testable import HumeAI + +final class HumeAIClientCustomVoiceTests: XCTestCase { + + func testListCustomVoices() async throws { + let voices = try await client.listCustomVoices() + XCTAssertNotNil(voices) + } + + func testCreateCustomVoice() async throws { + let voice = try await client.createCustomVoice( + name: "Test Voice", + baseVoice: "base-voice", + model: .prosody, + parameters: .init() + ) + XCTAssertEqual(voice.name, "Test Voice") + } + + func testDeleteCustomVoice() async throws { + try await client.deleteCustomVoice(id: "test-id") + } + + func testGetCustomVoice() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testCreateCustomVoiceVersion() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } + + func testUpdateCustomVoiceName() async throws { + print("Needs Implementation") + XCTFail("Not implemented") + } +} diff --git a/Tests/HumeAI/module.swift b/Tests/HumeAI/module.swift index 1c4d3b99..a0cddc5f 100644 --- a/Tests/HumeAI/module.swift +++ b/Tests/HumeAI/module.swift @@ -5,3 +5,10 @@ // Created by Jared Davidson on 11/22/24. // +import HumeAI + +let HUMEAI_API_KEY = "Ei8s58Zp0JWqH9g00N8LdOFnpu03H4uj1Nr300OAh5dsdiGr" + +var client = HumeAI.Client( + apiKey: HUMEAI_API_KEY +) From eb9f69cc607dd89f4e30e116bb778173693a73be Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 15:37:17 -0700 Subject: [PATCH 35/73] Testing --- ...umeAI.APISpecification.RequestBodies.swift | 14 +- .../API/HumeAI.APISpecification.swift | 141 ++++++------ ...eAI.APISpeicification.ResponseBodies.swift | 75 +----- .../Intramodular/HumeAI.Client-Batch.swift | 25 +- .../HumeAI.Client-CustomVoices.swift | 2 +- .../Intramodular/HumeAI.Client-Jobs.swift | 4 +- .../Intramodular/HumeAI.Client-Tools.swift | 34 ++- .../HumeAI/Intramodular/HumeAI.Model.swift | 167 +++++++++++++- .../Intramodular/Models/HumeAI.Job.swift | 109 +++++++-- .../Models/HumeAI.JobPrediction.swift | 67 ++++++ .../Intramodular/Models/HumeAI.Tool.swift | 7 +- Tests/HumeAI/Intramodular/Batch.swift | 24 +- Tests/HumeAI/Intramodular/Chat.swift | 8 +- Tests/HumeAI/Intramodular/Job.swift | 214 +++++++----------- Tests/HumeAI/Intramodular/Stream.swift | 2 +- Tests/HumeAI/Intramodular/Tools.swift | 74 +++++- Tests/HumeAI/Intramodular/Voices.swift | 2 +- 17 files changed, 627 insertions(+), 342 deletions(-) create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.JobPrediction.swift diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index 066e2695..7788010e 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -55,8 +55,8 @@ extension HumeAI.APISpecification { } struct BatchInferenceJobInput: Codable { - let files: [HumeAI.FileInput] - let models: [HumeAI.Model] + let urls: [URL] + let models: HumeAI.Model let callback: CallbackConfig? } @@ -238,14 +238,12 @@ extension HumeAI.APISpecification { } struct CreateToolInput: Codable { - let id: String + var id: String? = nil let name: String + let parameters: String + let versionDescription: String? let description: String? - let configuration: Configuration - - struct Configuration: Codable { - let parameters: [String: String] - } + let fallbackContent: String? } struct CreateToolVersionInput: Codable { diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 9890841c..06d14b68 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -61,260 +61,260 @@ extension HumeAI { // MARK: - Tools @GET - @Path("/v0/tools") + @Path("/v0/evi/tools") var listTools = Endpoint() @POST - @Path("/v0/tools") + @Path("/v0/evi/tools") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createTool = Endpoint() @GET @Path({ context -> String in - "/v0/tools/\(context.input.id)/versions" + "/v0/evi/tools/\(context.input.id)/versions" }) var listToolVersions = Endpoint() @POST @Path({ context -> String in - "/v0/tools/\(context.input.id)/versions" + "/v0/evi/tools/\(context.input.id ?? "")" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createToolVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/tools/\(context.input.id)" + "/v0/evi/tools/\(context.input.id)" }) var deleteTool = Endpoint() @PATCH @Path({ context -> String in - "/v0/tools/\(context.input.id)" + "/v0/evi/tools/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateToolName = Endpoint() @GET @Path({ context -> String in - "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionId)" }) var getToolVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/tools/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionId)" }) var deleteToolVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/tools/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionID)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateToolDescription = Endpoint() // MARK: - Prompts @GET - @Path("/v0/prompts") + @Path("/v0/evi/prompts") var listPrompts = Endpoint() @POST - @Path("/v0/prompts") + @Path("/v0/evi/prompts") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createPrompt = Endpoint() @GET @Path({ context -> String in - "/v0/prompts/\(context.input.id)/versions" + "/v0/evi/prompts/\(context.input.id)/versions" }) var listPromptVersions = Endpoint() @POST @Path({ context -> String in - "/v0/prompts/\(context.input.id)/versions" + "/v0/evi/prompts/\(context.input.id)/versions" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createPromptVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/prompts/\(context.input.id)" + "/v0/evi/prompts/\(context.input.id)" }) var deletePrompt = Endpoint() @PATCH @Path({ context -> String in - "/v0/prompts/\(context.input.id)" + "/v0/evi/prompts/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updatePromptName = Endpoint() @GET @Path({ context -> String in - "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) var getPromptVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/prompts/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionId)" }) var deletePromptVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/prompts/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionID)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updatePromptDescription = Endpoint() // MARK: - Custom Voices @GET - @Path("/v0/custom-voices") + @Path("/v0/evi/custom-voices") var listCustomVoices = Endpoint() @POST - @Path("/v0/custom-voices") + @Path("/v0/evi/custom-voices") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createCustomVoice = Endpoint() @GET @Path({ context -> String in - "/v0/custom-voices/\(context.input.id)" + "/v0/evi/custom-voices/\(context.input.id)" }) var getCustomVoice = Endpoint() @POST @Path({ context -> String in - "/v0/custom-voices/\(context.input.id)/versions" + "/v0/evi/custom-voices/\(context.input.id)/versions" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createCustomVoiceVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/custom-voices/\(context.input.id)" + "/v0/evi/custom-voices/\(context.input.id)" }) var deleteCustomVoice = Endpoint() @PATCH @Path({ context -> String in - "/v0/custom-voices/\(context.input.id)" + "/v0/evi/custom-voices/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateCustomVoiceName = Endpoint() // MARK: - Configs @GET - @Path("/v0/configs") + @Path("/v0/evi/configs") var listConfigs = Endpoint() @POST - @Path("/v0/configs") + @Path("/v0/evi/configs") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createConfig = Endpoint() @GET @Path({ context -> String in - "/v0/configs/\(context.input.id)/versions" + "/v0/evi/configs/\(context.input.id)/versions" }) var listConfigVersions = Endpoint() @POST @Path({ context -> String in - "/v0/configs/\(context.input.id)/versions" + "/v0/evi/configs/\(context.input.id)/versions" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createConfigVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/configs/\(context.input.id)" + "/v0/evi/configs/\(context.input.id)" }) var deleteConfig = Endpoint() @PATCH @Path({ context -> String in - "/v0/configs/\(context.input.id)" + "/v0/evi/configs/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateConfigName = Endpoint() @GET @Path({ context -> String in - "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionId)" }) var getConfigVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/configs/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionId)" }) var deleteConfigVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/configs/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionID)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateConfigDescription = Endpoint() // MARK: - Chats @GET - @Path("/v0/chats") + @Path("/v0/evi/chats") var listChats = Endpoint() @GET @Path({ context -> String in - "/v0/chats/\(context.input.id)/events" + "/v0/evi/chats/\(context.input.id)/events" }) var listChatEvents = Endpoint() @GET @Path({ context -> String in - "/v0/chats/\(context.input.id)/audio" + "/v0/evi/chats/\(context.input.id)/audio" }) var getChatAudio = Endpoint() // MARK: - Chat Groups @GET - @Path("/v0/chat-groups") + @Path("/v0/evi/chat-groups") var listChatGroups = Endpoint() @GET @Path({ context -> String in - "/v0/chat-groups/\(context.input.id)" + "/v0/evi/chat-groups/\(context.input.id)" }) var getChatGroup = Endpoint() @GET @Path({ context -> String in - "/v0/chat-groups/\(context.input.id)/events" + "/v0/evi/chat-groups/\(context.input.id)/events" }) var listChatGroupEvents = Endpoint() @GET @Path({ context -> String in - "/v0/chat-groups/\(context.input.id)/audio" + "/v0/evi/chat-groups/\(context.input.id)/audio" }) var getChatGroupAudio = Endpoint() // MARK: - Chat @POST - @Path("/v0/chat") + @Path("/v0/evi/chat") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var chat = Endpoint() // MARK: - Batch @GET @Path("/v0/batch/jobs") - var listJobs = Endpoint() + var listJobs = Endpoint() @POST @Path("/v0/batch/jobs") - @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var startInferenceJob = Endpoint() + @Body(json: \.input) + var startInferenceJob = Endpoint() @GET @Path({ context -> String in @@ -326,7 +326,7 @@ extension HumeAI { @Path({ context -> String in "/v0/batch/jobs/\(context.input.id)/predictions" }) - var getJobPredictions = Endpoint() + var getJobPredictions = Endpoint() @GET @Path({ context -> String in @@ -336,126 +336,126 @@ extension HumeAI { // MARK: - Stream @POST - @Path("/v0/stream") + @Path("/v0/stream/models") @Body(multipart: .input) var streamInference = Endpoint() // MARK: - Files @GET - @Path("/v0/files") + @Path("/v0/registry/files") var listFiles = Endpoint() @POST - @Path("/v0/files") + @Path("/v0/registry/files") @Body(multipart: .input) var uploadFile = Endpoint() @GET @Path({ context -> String in - "/v0/files/\(context.input.id)" + "/v0/registry/files/\(context.input.id)" }) var getFile = Endpoint() @DELETE @Path({ context -> String in - "/v0/files/\(context.input.id)" + "/v0/registry/files/\(context.input.id)" }) var deleteFile = Endpoint() @PATCH @Path({ context -> String in - "/v0/files/\(context.input.id)" + "/v0/registry/files/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateFileName = Endpoint() @GET @Path({ context -> String in - "/v0/files/\(context.input.id)/predictions" + "/v0/registry/files/\(context.input.id)/predictions" }) - var getFilePredictions = Endpoint() + var getFilePredictions = Endpoint() // MARK: - Datasets @GET - @Path("/v0/datasets") + @Path("/v0/registry/datasets") var listDatasets = Endpoint() @POST - @Path("/v0/datasets") + @Path("/v0/registry/datasets") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createDataset = Endpoint() @GET @Path({ context -> String in - "/v0/datasets/\(context.input.id)" + "/v0/registry/datasets/\(context.input.id)" }) var getDataset = Endpoint() @POST @Path({ context -> String in - "/v0/datasets/\(context.input.id)/versions" + "/v0/registry/datasets/\(context.input.id)/versions" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createDatasetVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/datasets/\(context.input.id)" + "/v0/registry/datasets/\(context.input.id)" }) var deleteDataset = Endpoint() @GET @Path({ context -> String in - "/v0/datasets/\(context.input.id)/versions" + "/v0/registry/datasets/\(context.input.id)/versions" }) var listDatasetVersions = Endpoint() // MARK: - Models @GET - @Path("/v0/models") + @Path("/v0/registry/models") var listModels = Endpoint() @GET @Path({ context -> String in - "/v0/models/\(context.input.id)" + "/v0/registry/models/\(context.input.id)" }) var getModel = Endpoint() @PATCH @Path({ context -> String in - "/v0/models/\(context.input.id)" + "/v0/registry/models/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateModelName = Endpoint() @GET @Path({ context -> String in - "/v0/models/\(context.input.id)/versions" + "/v0/registry/models/\(context.input.id)/versions" }) var listModelVersions = Endpoint() @GET @Path({ context -> String in - "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/registry/models/\(context.input.id)/versions/\(context.input.versionId)" }) var getModelVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/models/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/registry/models/\(context.input.id)/versions/\(context.input.versionId)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateModelDescription = Endpoint() // MARK: - Jobs @POST - @Path("/v0/jobs/training") + @Path("/v0/registry/v0/batch/jobs/tl/train") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var startTrainingJob = Endpoint() + var startTrainingJob = Endpoint() @POST - @Path("/v0/jobs/inference") + @Path("/v0/batch/jobs/tl/inference") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var startCustomInferenceJob = Endpoint() + var startCustomInferenceJob = Endpoint() } } @@ -486,7 +486,6 @@ extension HumeAI.APISpecification { } request = request - .header("Accept", "application/json") .header("X-Hume-Api-Key", apiKey) .header(.contentType(.json)) @@ -497,6 +496,8 @@ extension HumeAI.APISpecification { from response: HTTPResponse, context: DecodeOutputContext ) throws -> Output { + + print(response) do { try response.validate() } catch { diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index a4ad60d3..5ce1f6ab 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -102,13 +102,6 @@ extension HumeAI.APISpecification { let pageSize: Int let totalPages: Int let chatGroups: [ChatGroup] - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case totalPages = "total_pages" - case chatGroups = "chat_groups_page" - } } struct Chat: Codable { @@ -273,85 +266,27 @@ extension HumeAI.APISpecification { } } - struct Tool: Codable { - let id: String - let name: String - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 - let versions: [ToolVersion]? - } - struct ToolVersion: Codable { let id: String - let toolId: String + let version: Int + let toolId: String? let description: String? let createdOn: Int64 let modifiedOn: Int64 - let configuration: Configuration - - struct Configuration: Codable { - let parameters: [String: String] - } } struct ToolList: Codable { let pageNumber: Int let pageSize: Int let totalPages: Int - let tools: [HumeAI.Tool] - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case totalPages = "total_pages" - case tools = "tools_page" - } + let toolsPage: [HumeAI.Tool] } - - struct Job: Codable { - let id: String - let status: String - let createdOn: Int64 - let modifiedOn: Int64 - let predictions: [Prediction]? - let artifacts: [String: String]? - - struct Prediction: Codable { - let file: FileInfo - let results: [ModelResult] - - struct FileInfo: Codable { - let url: String - let mimeType: String - let metadata: [String: String]? - - private enum CodingKeys: String, CodingKey { - case url - case mimeType = "mime_type" - case metadata - } - } - - struct ModelResult: Codable { - let model: String - let results: [String: String] - } - } - } - + struct JobList: Codable { let pageNumber: Int let pageSize: Int let totalPages: Int - let jobs: [Job] - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case totalPages = "total_pages" - case jobs = "jobs_page" - } + let jobs: [HumeAI.Job] } } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift index f8eba3b7..97dca70d 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift @@ -11,11 +11,11 @@ import Merge extension HumeAI.Client { public func startInferenceJob( - files: [HumeAI.FileInput], - models: [HumeAI.Model] - ) async throws -> HumeAI.Job { + urls: [URL], + models: HumeAI.Model + ) async throws -> HumeAI.JobID { let input = HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput( - files: files.map { .init(url: $0.url, mimeType: $0.mimeType, metadata: $0.metadata) }, + urls: urls, models: models, callback: nil ) @@ -30,4 +30,21 @@ extension HumeAI.Client { ) return try await run(\.getJobDetails, with: input) } + + public func getJobPredictions( + id: String + ) async throws -> [HumeAI.JobPrediction] { + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.getJobPredictions, with: input) + } + public func listJobs() async throws -> [HumeAI.Job] { + return try await run(\.listJobs, with: ()) + } + + public func getJobArtifacts(id: String) async throws -> [String: String] { + let input = HumeAI.APISpecification.PathInput.ID(id: id) + return try await run(\.getJobArtifacts, with: input) + } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift index 8751e6c7..caadc511 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift @@ -24,7 +24,7 @@ extension HumeAI.Client { let input = HumeAI.APISpecification.RequestBodies.CreateVoiceInput( name: name, baseVoice: baseVoice, - parameterModel: model.rawValue, + parameterModel: HumeAI.paramaterModel, parameters: parameters ) return try await run(\.createCustomVoice, with: input) diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift index c96f9527..5ff142f6 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Jobs.swift @@ -15,7 +15,7 @@ extension HumeAI.Client { name: String, description: String? = nil, configuration: [String: String] - ) async throws -> HumeAI.Job { + ) async throws -> HumeAI.JobID { let input = HumeAI.APISpecification.RequestBodies.TrainingJobInput( datasetId: datasetId, name: name, @@ -29,7 +29,7 @@ extension HumeAI.Client { modelId: String, files: [HumeAI.FileInput], configuration: [String: String] - ) async throws -> HumeAI.Job { + ) async throws -> HumeAI.JobID { let input = HumeAI.APISpecification.RequestBodies.CustomInferenceJobInput( modelId: modelId, files: files, diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift index c09e9b36..2a58f401 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift @@ -12,7 +12,7 @@ import Merge extension HumeAI.Client { public func listTools() async throws -> [HumeAI.Tool] { let response = try await run(\.listTools) - return response.tools + return response.toolsPage } public func createTool( @@ -21,13 +21,33 @@ extension HumeAI.Client { description: String?, configuration: [String: String] ) async throws -> HumeAI.Tool { - let input = HumeAI.APISpecification.RequestBodies.CreateToolInput( - id: id, - name: name, - description: description, - configuration: .init(parameters: configuration) + let parameters: [String: Any] = [ + "type": "object", + "properties": [ + "location": [ + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + ], + "format": [ + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location." + ] + ], + "required": ["location", "format"] + ] + + let jsonParameters = try JSONSerialization.data(withJSONObject: parameters) + let parametersString = String(data: jsonParameters, encoding: .utf8) ?? "{}" + + let tool = HumeAI.APISpecification.RequestBodies.CreateToolInput( + name: "get_current_weather", + parameters: parametersString, + versionDescription: "Fetches current weather and uses celsius or fahrenheit based on location of user.", + description: "This tool is for getting the current weather.", + fallbackContent: "Unable to fetch current weather." ) - return try await run(\.createTool, with: input) + return try await run(\.createTool, with: tool) } public func deleteTool( diff --git a/Sources/HumeAI/Intramodular/HumeAI.Model.swift b/Sources/HumeAI/Intramodular/HumeAI.Model.swift index db0292d4..c8723ca9 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Model.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Model.swift @@ -11,12 +11,165 @@ import Foundation import Swift extension HumeAI { - public enum Model: String, Codable, Sendable { - case prosody = "prosody" - case language = "language" - case burst = "burst" - case face = "face" - case speech = "speech" - case tts = "tts" + static let paramaterModel = "20241004-11parameter" +} + +extension HumeAI { + public struct Model: Codable { + public var face: Face? + public var burst: [String: String]? + public var prosody: Prosody? + public var language: Language? + public var ner: NER? + public var facemesh: [String: String]? + + public struct Face: Codable { + public var fpsPred: Double? + public var probThreshold: Double? + public var identifyFaces: Bool? + public var minFaceSize: UInt64? + public var facs: [String: String]? + public var descriptions: [String: String]? + public var saveFaces: Bool? + + public init( + fpsPred: Double? = 3.0, + probThreshold: Double? = 0.99, + identifyFaces: Bool? = false, + minFaceSize: UInt64? = nil, + facs: [String: String]? = nil, + descriptions: [String: String]? = nil, + saveFaces: Bool? = false + ) { + self.fpsPred = fpsPred + self.probThreshold = probThreshold + self.identifyFaces = identifyFaces + self.minFaceSize = minFaceSize + self.facs = facs + self.descriptions = descriptions + self.saveFaces = saveFaces + } + + enum CodingKeys: String, CodingKey { + case fpsPred = "fps_pred" + case probThreshold = "prob_threshold" + case identifyFaces = "identify_faces" + case minFaceSize = "min_face_size" + case facs + case descriptions + case saveFaces = "save_faces" + } + } + + public struct Prosody: Codable { + public enum Granularity: String, Codable { + case word + case sentence + case utterance + case conversationalTurn = "conversational_turn" + } + + public struct Window: Codable { + public var length: Double? + public var step: Double? + + public init(length: Double? = 4.0, step: Double? = 1.0) { + self.length = length + self.step = step + } + } + + public var granularity: Granularity? + public var window: Window? + public var identifySpeakers: Bool? + + public init( + granularity: Granularity? = nil, + window: Window? = nil, + identifySpeakers: Bool? = false + ) { + self.granularity = granularity + self.window = window + self.identifySpeakers = identifySpeakers + } + + enum CodingKeys: String, CodingKey { + case granularity + case window + case identifySpeakers = "identify_speakers" + } + } + + public struct Language: Codable { } + + public struct NER: Codable { } + + public init( + face: Face? = nil, + burst: [String: String]? = nil, + prosody: Prosody? = nil, + language: Language? = nil, + ner: NER? = nil, + facemesh: [String: String]? = nil + ) { + self.face = face + self.burst = burst + self.prosody = prosody + self.language = language + self.ner = ner + self.facemesh = facemesh + } + } +} + +// Helper initializers for simpler model creation +extension HumeAI.Model { + public static func face( + fpsPred: Double? = 3.0, + probThreshold: Double? = 0.99, + identifyFaces: Bool? = false, + minFaceSize: UInt64? = nil, + facs: [String: String]? = [:], + descriptions: [String: String]? = [:], + saveFaces: Bool? = false + ) -> Self { + .init(face: .init( + fpsPred: fpsPred, + probThreshold: probThreshold, + identifyFaces: identifyFaces, + minFaceSize: minFaceSize, + facs: facs, + descriptions: descriptions, + saveFaces: saveFaces + )) + } + + public static func burst() -> Self { + .init(burst: [:]) + } + + public static func prosody( + granularity: Prosody.Granularity? = nil, + windowLength: Double? = 4.0, + windowStep: Double? = 1.0, + identifySpeakers: Bool? = false + ) -> Self { + .init(prosody: .init( + granularity: granularity, + window: .init(length: windowLength, step: windowStep), + identifySpeakers: identifySpeakers + )) + } + + public static func language() -> Self { + .init(language: .init()) + } + + public static func ner() -> Self { + .init(ner: .init()) + } + + public static func facemesh() -> Self { + .init(facemesh: [:]) } } diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift index 5617d7bd..5d9587f3 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift @@ -6,28 +6,105 @@ // extension HumeAI { - public struct Job { - public let id: String + // MARK: - Root Response + public struct Job: Codable { + public let state: JobState + public let userID: String + public let type: String + public let jobID: String + public let request: JobRequest + + enum CodingKeys: String, CodingKey { + case state + case userID = "userId" + case type + case jobID = "jobId" + case request + } + } + + // MARK: - Job State + public struct JobState: Codable { + public let endedTimestampMs: Int64 + public let createdTimestampMs: Int64 + public let numPredictions: Int public let status: String - public let createdOn: Int64 - public let modifiedOn: Int64 - public let predictions: [Prediction]? - public let artifacts: [String: String]? + public let numErrors: Int + public let startedTimestampMs: Int64 + } + + // MARK: - Job Request + public struct JobRequest: Codable { + public let models: Models + public let notify: Bool + public let urls: [String] + public let callbackUrl: String? + public let text: [String] + public let files: [String] + public let registryFiles: [String] + } + + // MARK: - Models + public struct Models: Codable { + public let burst: [String: [String: String]]? + public let facemesh: JSON? + public let language: JSON? + public let face: JSON? + public let ner: JSON? + public let prosody: JSON? - public struct Prediction { - public let file: FileInfo - public let results: [ModelResult] + public enum JSON: Codable { + case null + case bool(Bool) + case number(Double) + case string(String) + case array([JSON]) + case object([String: JSON]) - public struct FileInfo { - public let url: String - public let mimeType: String - public let metadata: [String: String]? + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let number = try? container.decode(Double.self) { + self = .number(number) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let array = try? container.decode([JSON].self) { + self = .array(array) + } else if let object = try? container.decode([String: JSON].self) { + self = .object(object) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid JSON value") + } } - public struct ModelResult { - public let model: String - public let results: [String: String] + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: + try container.encodeNil() + case .bool(let bool): + try container.encode(bool) + case .number(let number): + try container.encode(number) + case .string(let string): + try container.encode(string) + case .array(let array): + try container.encode(array) + case .object(let object): + try container.encode(object) + } } } } + + public struct JobID: Codable { + public let jobID: String + + enum CodingKeys: String, CodingKey { + case jobID = "jobId" + } + } } diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.JobPrediction.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.JobPrediction.swift new file mode 100644 index 00000000..3a6aae61 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.JobPrediction.swift @@ -0,0 +1,67 @@ +// +// HumeAI.JobPrediction.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +import Foundation + +extension HumeAI { + public struct JobPrediction: Codable { + public let source: Source + public let results: Results + + public struct Source: Codable { + public let type: String + public let url: String + } + + public struct Results: Codable { + public let predictions: [Prediction] + public let errors: [String] + } + + public struct Prediction: Codable { + public let file: String + public let models: Models + } + + public struct Models: Codable { + public let face: FaceModel? + + public struct FaceModel: Codable { + public let groupedPredictions: [GroupedPrediction] + + enum CodingKeys: String, CodingKey { + case groupedPredictions = "grouped_predictions" + } + } + } + + public struct GroupedPrediction: Codable { + public let id: String + public let predictions: [FacePrediction] + } + + public struct FacePrediction: Codable { + public let frame: Int + public let time: Int + public let prob: Double + public let box: BoundingBox + public let emotions: [Emotion] + } + + public struct BoundingBox: Codable { + public let x: Double + public let y: Double + public let w: Double + public let h: Double + } + + public struct Emotion: Codable { + public let name: String + public let score: Double + } + } +} diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift index aa632f7c..244e2656 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift @@ -20,11 +20,8 @@ extension HumeAI { public let description: String? public let createdOn: Int64 public let modifiedOn: Int64 - public let configuration: Configuration - - public struct Configuration: Codable { - public let parameters: [String: String] - } + public let parameters: String + public let fallbackContent: String? } } } diff --git a/Tests/HumeAI/Intramodular/Batch.swift b/Tests/HumeAI/Intramodular/Batch.swift index c241591f..406ddcfc 100644 --- a/Tests/HumeAI/Intramodular/Batch.swift +++ b/Tests/HumeAI/Intramodular/Batch.swift @@ -11,36 +11,30 @@ import XCTest final class HumeAIClientBatchTests: XCTestCase { func testStartInferenceJob() async throws { let job = try await client.startInferenceJob( - files: [.init( - url: "test-url", - mimeType: "test/mime" - )], - models: [.burst] + urls: [URL(string: "https://hume-tutorials.s3.amazonaws.com/faces.zip")!], + models: .burst() ) XCTAssertNotNil(job) } func testGetJobDetails() async throws { - let job = try await client.getJobDetails(id: "test-id") + let job = try await client.getJobDetails(id: "424ddd20-b604-435b-abb0-712f1fe9303b") XCTAssertNotNil(job) } func testListJobs() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let jobs = try await client.listJobs() + XCTAssertNotNil(jobs) } func testGetJobPredictions() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let predictions = try await client.getJobPredictions(id: "424ddd20-b604-435b-abb0-712f1fe9303b") + XCTAssertNotNil(predictions) } func testGetJobArtifacts() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testStartInferenceJobFromLocalFile() async throws { + //Get the artifacts ZIP of a completed inference job. + print("Needs Implementation") XCTFail("Not implemented") } diff --git a/Tests/HumeAI/Intramodular/Chat.swift b/Tests/HumeAI/Intramodular/Chat.swift index 7708d3c2..a93a3c15 100644 --- a/Tests/HumeAI/Intramodular/Chat.swift +++ b/Tests/HumeAI/Intramodular/Chat.swift @@ -26,7 +26,13 @@ final class HumeAIClientChatTests: XCTestCase { } func testChat() async throws { - let response = try await client.chat(messages: [.init(role: "user", content: "Hello")], model: "test-model") + let response = try await client.chat( + messages: [.init( + role: "user", + content: "Hello" + )], + model: "test-model" + ) XCTAssertNotNil(response) } } diff --git a/Tests/HumeAI/Intramodular/Job.swift b/Tests/HumeAI/Intramodular/Job.swift index 39464877..437e0251 100644 --- a/Tests/HumeAI/Intramodular/Job.swift +++ b/Tests/HumeAI/Intramodular/Job.swift @@ -10,159 +10,119 @@ import XCTest final class HumeAIClientJobTests: XCTestCase { - // Training Jobs - func testStartTrainingJob() async throws { - let job = try await client.startTrainingJob( - datasetId: "test-id", - name: "Test Training Job", - description: "Test Description", - configuration: ["key": "value"] - ) - XCTAssertNotNil(job) - XCTAssertEqual(job.status, "pending") // Assuming initial status is pending - XCTAssertNotNil(job.id) - } - - func testStartTrainingJobWithoutDescription() async throws { - let job = try await client.startTrainingJob( - datasetId: "test-id", - name: "Test Training Job", - configuration: ["key": "value"] - ) - XCTAssertNotNil(job) - } - - // Custom Inference Jobs - func testStartCustomInferenceJob() async throws { - let files = [ - HumeAI.FileInput( - url: "test-url", - mimeType: "test/mime", - metadata: ["key": "value"] - ) - ] + // MARK: - Inference Jobs + func testStartInferenceJob() async throws { - let job = try await client.startCustomInferenceJob( - modelId: "test-id", - files: files, - configuration: ["key": "value"] + let response = try await client.startInferenceJob( + urls: [URL(string:"https://hume-tutorials.s3.amazonaws.com/faces.zip")!], + models: .burst() ) - XCTAssertNotNil(job) - XCTAssertNotNil(job.id) + + // Test JobResponse structure + XCTAssertNotNil(response) } - // Job Status and Progress + // MARK: - Job Status and Progress func testGetJobDetails() async throws { - let job = try await client.getJobDetails(id: "test-id") - XCTAssertNotNil(job) - XCTAssertNotNil(job.status) - XCTAssertNotNil(job.createdOn) - XCTAssertNotNil(job.modifiedOn) + let response = try await client.getJobDetails(id: "test-id") + + // Test timestamps + XCTAssertGreaterThan(response.state.endedTimestampMs, response.state.startedTimestampMs) + XCTAssertGreaterThan(response.state.endedTimestampMs, response.state.createdTimestampMs) + + // Test status + XCTAssertEqual(response.state.status, "COMPLETED") + + // Test request data + XCTAssertNotNil(response.request.urls) + XCTAssertFalse(response.request.notify) } - // Error Cases - func testStartTrainingJobWithInvalidDataset() async throws { - do { - _ = try await client.startTrainingJob( - datasetId: "invalid-id", - name: "Test Job", - configuration: ["key": "value"] - ) - XCTFail("Expected error for invalid dataset ID") - } catch { - // Expected error - XCTAssertNotNil(error) + // MARK: - Job Predictions + func testGetJobPredictions() async throws { + let predictions = try await client.getJobPredictions(id: "test-id") + + guard let firstPrediction = predictions.first else { + XCTFail("No predictions found") + return + } + + // Test source + XCTAssertEqual(firstPrediction.source.type, "url") + XCTAssertNotNil(firstPrediction.source.url) + + // Test results structure + XCTAssertNotNil(firstPrediction.results.predictions) + XCTAssertTrue(firstPrediction.results.errors.isEmpty) + + // Test face predictions + if let facePrediction = firstPrediction.results.predictions.first?.models.face?.groupedPredictions.first?.predictions.first { + // Test bounding box + XCTAssertGreaterThan(facePrediction.prob, 0) + XCTAssertNotNil(facePrediction.box) + + // Test emotions + XCTAssertFalse(facePrediction.emotions.isEmpty) + + // Test specific emotions exist + let emotionNames = facePrediction.emotions.map { $0.name } + XCTAssertTrue(emotionNames.contains("Joy")) + XCTAssertTrue(emotionNames.contains("Fear")) + + // Test emotion scores + for emotion in facePrediction.emotions { + XCTAssertGreaterThanOrEqual(emotion.score, 0) + XCTAssertLessThanOrEqual(emotion.score, 1) + } + } else { + XCTFail("No face predictions found") } } - func testStartCustomInferenceJobWithInvalidModel() async throws { + // MARK: - Error Cases + func testInvalidJobId() async throws { do { - _ = try await client.startCustomInferenceJob( - modelId: "invalid-id", - files: [.init(url: "test-url", mimeType: "test/mime")], - configuration: ["key": "value"] - ) - XCTFail("Expected error for invalid model ID") + _ = try await client.getJobDetails(id: "invalid-id") + XCTFail("Expected error for invalid job ID") } catch { - // Expected error XCTAssertNotNil(error) } } - func testGetInvalidJobDetails() async throws { + func testInvalidFileUrl() async throws { do { - _ = try await client.getJobDetails(id: "invalid-id") - XCTFail("Expected error for invalid job ID") + _ = try await client.startInferenceJob( + urls: [URL(string:"https://hume-tutorials.s3.amazonaws.com/faces.zip")!], + models: .burst() + ) + XCTFail("Expected error for invalid file URL") } catch { - // Expected error XCTAssertNotNil(error) } } - // Job Results - func testJobPredictions() async throws { - let job = try await client.getJobDetails(id: "test-id") + // MARK: - Helper Methods + func testGetTopEmotions() async throws { + let predictions = try await client.getJobPredictions(id: "test-id") - if let predictions = job.predictions { - for prediction in predictions { - // Validate file info - XCTAssertNotNil(prediction.file.url) - XCTAssertNotNil(prediction.file.mimeType) - - // Validate results - XCTAssertFalse(prediction.results.isEmpty) - for result in prediction.results { - XCTAssertNotNil(result.model) - XCTAssertNotNil(result.results) - } - } + guard let facePrediction = predictions.first?.results.predictions.first?.models.face?.groupedPredictions.first?.predictions.first else { + XCTFail("No face predictions found") + return } - } - - // Job Artifacts - func testJobArtifacts() async throws { - let job = try await client.getJobDetails(id: "test-id") - if let artifacts = job.artifacts { - XCTAssertFalse(artifacts.isEmpty) - } - } - - // Unimplemented Methods Tests - func testListJobs() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testGetJobPredictions() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testGetJobArtifacts() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testStartInferenceJobFromLocalFile() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - // Job Status Transitions - func testJobStatusTransitions() async throws { - let job = try await client.getJobDetails(id: "test-id") + // Get top 3 emotions + let topEmotions = facePrediction.emotions + .sorted { $0.score > $1.score } + .prefix(3) - // Verify status is a valid value - let validStatuses = ["pending", "running", "completed", "failed"] - XCTAssertTrue(validStatuses.contains(job.status)) - } - - // Validation Tests - func testJobTimestamps() async throws { - let job = try await client.getJobDetails(id: "test-id") + XCTAssertEqual(topEmotions.count, 3) - // Created timestamp should be before or equal to modified timestamp - XCTAssertLessThanOrEqual(job.createdOn, job.modifiedOn) + // Verify they're actually the highest scores + let highestScore = topEmotions.first!.score + for emotion in facePrediction.emotions { + if emotion.name != topEmotions.first!.name { + XCTAssertLessThanOrEqual(emotion.score, highestScore) + } + } } } diff --git a/Tests/HumeAI/Intramodular/Stream.swift b/Tests/HumeAI/Intramodular/Stream.swift index b27e4539..effbd364 100644 --- a/Tests/HumeAI/Intramodular/Stream.swift +++ b/Tests/HumeAI/Intramodular/Stream.swift @@ -14,7 +14,7 @@ final class HumeAIClientStreamTests: XCTestCase { let job = try await client.streamInference( id: "test-id", file: Data(), - models: [.language] + models: [.language()] ) XCTAssertNotNil(job) } diff --git a/Tests/HumeAI/Intramodular/Tools.swift b/Tests/HumeAI/Intramodular/Tools.swift index e888c2e5..216d6abd 100644 --- a/Tests/HumeAI/Intramodular/Tools.swift +++ b/Tests/HumeAI/Intramodular/Tools.swift @@ -11,27 +11,87 @@ import XCTest final class HumeAIClientToolTests: XCTestCase { func testListTools() async throws { + let tool = try await client.createTool( + id: UUID().uuidString, + name: "Test Tool", + description: "Test Description", + configuration: [:] + ) let tools = try await client.listTools() XCTAssertNotNil(tools) + try await client.deleteTool(id: tool.id) } func testCreateTool() async throws { - let tool = try await client.createTool(id: "test-id", name: "Test Tool", description: "Test Description", configuration: ["key": "value"]) - XCTAssertEqual(tool.name, "Test Tool") + let tool = try await client.createTool( + id: UUID().uuidString, + name: "Test Tool", + description: "Test Description", + configuration: [:] + ) + try await client.deleteTool(id: tool.id) + XCTAssertEqual(tool.name, "get_current_weather") } func testListToolVersions() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + createToolVersion() + let toolVersions = try await client.run(\.listToolVersions, with: .init(id: "123")) + XCTAssertNotNil(toolVersions) + } + + func createToolVersion() async throws -> HumeAI.ToolVersion { + let tool = try await client.createTool( + id: UUID().uuidString, + name: "Test Tool", + description: "Test Description", + configuration: [:] + ) + + let parameters: [String: Any] = [ + "type": "object", + "properties": [ + "location": [ + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + ], + "format": [ + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location." + ] + ], + "required": ["location", "format"] + ] + + let jsonParameters = try JSONSerialization.data(withJSONObject: parameters) + let parametersString = String(data: jsonParameters, encoding: .utf8) ?? "{}" + + let toolVersion = try await client.run( + \.createToolVersion, + with: .init( + id: tool.id, + name: "get_current_weather", + parameters: parametersString, + versionDescription: "Fetches current weather and uses celsius or fahrenheit based on location of user.", + description: "This tool is for getting the current weather.", + fallbackContent: "Unable to fetch current weather." + ) + ) } func testCreateToolVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + createToolVersion() + try await client.deleteTool(id: tool.id) } func testDeleteTool() async throws { - try await client.deleteTool(id: "test-id") + let tool = try await client.createTool( + id: UUID().uuidString, + name: "Test Tool", + description: "Test Description", + configuration: [:] + ) + try await client.deleteTool(id: tool.id) } func testUpdateToolName() async throws { diff --git a/Tests/HumeAI/Intramodular/Voices.swift b/Tests/HumeAI/Intramodular/Voices.swift index 6ac53a3e..d0f0c70a 100644 --- a/Tests/HumeAI/Intramodular/Voices.swift +++ b/Tests/HumeAI/Intramodular/Voices.swift @@ -19,7 +19,7 @@ final class HumeAIClientCustomVoiceTests: XCTestCase { let voice = try await client.createCustomVoice( name: "Test Voice", baseVoice: "base-voice", - model: .prosody, + model: HumeAI.Model.prosody(), parameters: .init() ) XCTAssertEqual(voice.name, "Test Voice") From c3976a8f8e93dc641d0efc760a0c5e194a55445f Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 16:25:36 -0700 Subject: [PATCH 36/73] Updated tests --- ...umeAI.APISpecification.RequestBodies.swift | 2 +- .../API/HumeAI.APISpecification.swift | 38 +++--- ...eAI.APISpeicification.ResponseBodies.swift | 12 +- .../Intramodular/HumeAI.Client-Tools.swift | 17 ++- .../Intramodular/Models/HumeAI.Tool.swift | 13 +- Tests/HumeAI/Intramodular/Prompts.swift | 6 +- Tests/HumeAI/Intramodular/Tools.swift | 117 +++++++++++------- 7 files changed, 120 insertions(+), 85 deletions(-) diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index 7788010e..445ad0e1 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -263,7 +263,7 @@ extension HumeAI.APISpecification { struct UpdateToolDescriptionInput: Codable { let id: String - let versionID: String + let version: Int let description: String } } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 06d14b68..75c848db 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -71,16 +71,16 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/tools/\(context.input.id)/versions" + "/v0/evi/tools/\(context.input.id)" }) - var listToolVersions = Endpoint() + var listToolVersions = Endpoint() @POST @Path({ context -> String in "/v0/evi/tools/\(context.input.id ?? "")" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var createToolVersion = Endpoint() + var createToolVersion = Endpoint() @DELETE @Path({ context -> String in @@ -93,26 +93,26 @@ extension HumeAI { "/v0/evi/tools/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updateToolName = Endpoint() + var updateToolName = Endpoint() @GET @Path({ context -> String in - "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/tools/\(context.input.id)/version/\(context.input.version)" }) - var getToolVersion = Endpoint() + var getToolVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/tools/\(context.input.id)/version/\(context.input.version)" }) var deleteToolVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/evi/tools/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/tools/\(context.input.id)/version/\(context.input.version)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updateToolDescription = Endpoint() + var updateToolDescription = Endpoint() // MARK: - Prompts @GET @@ -152,13 +152,13 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.version)" }) var getPromptVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.version)" }) var deletePromptVersion = Endpoint() @@ -243,13 +243,13 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/configs/\(context.input.id)/versions/\(context.input.version)" }) var getConfigVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/evi/configs/\(context.input.id)/versions/\(context.input.version)" }) var deleteConfigVersion = Endpoint() @@ -279,24 +279,24 @@ extension HumeAI { // MARK: - Chat Groups @GET - @Path("/v0/evi/chat-groups") + @Path("/v0/evi/chat_groups") var listChatGroups = Endpoint() @GET @Path({ context -> String in - "/v0/evi/chat-groups/\(context.input.id)" + "/v0/evi/chat_groups/\(context.input.id)" }) var getChatGroup = Endpoint() @GET @Path({ context -> String in - "/v0/evi/chat-groups/\(context.input.id)/events" + "/v0/evi/chat_groups/\(context.input.id)/events" }) var listChatGroupEvents = Endpoint() @GET @Path({ context -> String in - "/v0/evi/chat-groups/\(context.input.id)/audio" + "/v0/evi/chat_groups/\(context.input.id)/audio" }) var getChatGroupAudio = Endpoint() @@ -435,7 +435,7 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/registry/models/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/registry/models/\(context.input.id)/versions/\(context.input.version)" }) var getModelVersion = Endpoint() @@ -467,7 +467,7 @@ extension HumeAI.APISpecification { struct IDWithVersion: Codable { let id: String - let versionId: String + let version: Int } } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index 5ce1f6ab..44f12a81 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -266,13 +266,11 @@ extension HumeAI.APISpecification { } } - struct ToolVersion: Codable { - let id: String - let version: Int - let toolId: String? - let description: String? - let createdOn: Int64 - let modifiedOn: Int64 + struct ToolVersionList: Codable { + let pageNumber: Int + let pageSize: Int + let totalPages: Int + let toolsPage: [HumeAI.Tool.ToolVersion] } struct ToolList: Codable { diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift index 2a58f401..41e508dd 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Tools.swift @@ -59,14 +59,27 @@ extension HumeAI.Client { try await run(\.deleteTool, with: input) } + public func deleteToolVersion( + id: String, + version: Int + ) async throws { + + let input = HumeAI.APISpecification.PathInput.IDWithVersion( + id: id, + version: version + ) + + try await run(\.deleteToolVersion, with: input) + } + public func updateToolName( id: String, name: String - ) async throws -> HumeAI.Tool { + ) async throws { let input = HumeAI.APISpecification.RequestBodies.UpdateToolNameInput( id: id, name: name ) - return try await run(\.updateToolName, with: input) + try await run(\.updateToolName, with: input) } } diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift index 244e2656..24cf1df7 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift @@ -15,13 +15,12 @@ extension HumeAI { public let versions: [ToolVersion]? public struct ToolVersion: Codable { - public let id: String - public let toolId: String - public let description: String? - public let createdOn: Int64 - public let modifiedOn: Int64 - public let parameters: String - public let fallbackContent: String? + let id: String + let version: Int + let toolId: String? + let description: String? + let createdOn: Int64 + let modifiedOn: Int64 } } } diff --git a/Tests/HumeAI/Intramodular/Prompts.swift b/Tests/HumeAI/Intramodular/Prompts.swift index abac71ce..c7907ce5 100644 --- a/Tests/HumeAI/Intramodular/Prompts.swift +++ b/Tests/HumeAI/Intramodular/Prompts.swift @@ -16,7 +16,11 @@ final class HumeAIClientPromptTests: XCTestCase { } func testCreatePrompt() async throws { - let prompt = try await client.createPrompt(name: "Test Prompt", content: "Test Content", description: "Test Description") + let prompt = try await client.createPrompt( + name: "Test Prompt", + content: "Test Content", + description: "Test Description" + ) XCTAssertEqual(prompt.name, "Test Prompt") } diff --git a/Tests/HumeAI/Intramodular/Tools.swift b/Tests/HumeAI/Intramodular/Tools.swift index 216d6abd..7bc8d461 100644 --- a/Tests/HumeAI/Intramodular/Tools.swift +++ b/Tests/HumeAI/Intramodular/Tools.swift @@ -11,35 +11,88 @@ import XCTest final class HumeAIClientToolTests: XCTestCase { func testListTools() async throws { - let tool = try await client.createTool( - id: UUID().uuidString, - name: "Test Tool", - description: "Test Description", - configuration: [:] - ) + let tool = try await createTool() let tools = try await client.listTools() XCTAssertNotNil(tools) try await client.deleteTool(id: tool.id) } func testCreateTool() async throws { + let tool = try await createTool() + try await client.deleteTool(id: tool.id) + XCTAssertEqual(tool.name, "get_current_weather") + } + + func testListToolVersions() async throws { + let toolVersion = try await createToolVersion() + let toolVersions = try await client.run(\.listToolVersions, with: .init(id: toolVersion.tool.id)) + try await client.deleteTool(id: toolVersion.tool.id) + XCTAssertNotNil(toolVersions) + } + + func testCreateToolVersion() async throws { + let toolVersion = try await createToolVersion() + try await client.deleteTool(id: toolVersion.toolVersion.id) + } + + func testDeleteTool() async throws { + let tool = try await createTool() + try await client.deleteTool(id: tool.id) + } + + func testUpdateToolName() async throws { + let tool = try await createTool() + try await client.updateToolName(id: tool.id, name: "Updated_Name") + try await client.deleteTool(id: tool.id) + } + + func testGetToolVersion() async throws { + let toolVersion = try await createToolVersion() + let updatedTool = try await client.run( + \.getToolVersion, + with: .init( + id: toolVersion.tool.id, + version: toolVersion.toolVersion.version + ) + ) + try await client.deleteTool(id: updatedTool.id) + } + + func testDeleteToolVersion() async throws { + let toolVersion = try await createToolVersion() + try await client.deleteToolVersion( + id: toolVersion.tool.id, + version: toolVersion.toolVersion.version + ) + try await client.deleteTool(id: toolVersion.tool.id) + } + + func testUpdateToolDescription() async throws { + let toolVersion = try await createToolVersion() + try await client.run( + \.updateToolDescription, + with: .init( + id: toolVersion.tool.id, + version: toolVersion.toolVersion.version, + description: "Updated Description" + ) + ) + try await client.deleteTool(id: toolVersion.tool.id) + } + + func createTool() async throws -> HumeAI.Tool { let tool = try await client.createTool( id: UUID().uuidString, name: "Test Tool", description: "Test Description", configuration: [:] ) - try await client.deleteTool(id: tool.id) - XCTAssertEqual(tool.name, "get_current_weather") + + return tool } - func testListToolVersions() async throws { - createToolVersion() - let toolVersions = try await client.run(\.listToolVersions, with: .init(id: "123")) - XCTAssertNotNil(toolVersions) - } - func createToolVersion() async throws -> HumeAI.ToolVersion { + func createToolVersion() async throws -> (tool: HumeAI.Tool, toolVersion: HumeAI.Tool.ToolVersion) { let tool = try await client.createTool( id: UUID().uuidString, name: "Test Tool", @@ -77,40 +130,8 @@ final class HumeAIClientToolTests: XCTestCase { fallbackContent: "Unable to fetch current weather." ) ) + + return (tool, toolVersion) } - func testCreateToolVersion() async throws { - createToolVersion() - try await client.deleteTool(id: tool.id) - } - - func testDeleteTool() async throws { - let tool = try await client.createTool( - id: UUID().uuidString, - name: "Test Tool", - description: "Test Description", - configuration: [:] - ) - try await client.deleteTool(id: tool.id) - } - - func testUpdateToolName() async throws { - let tool = try await client.updateToolName(id: "test-id", name: "Updated Name") - XCTAssertEqual(tool.name, "Updated Name") - } - - func testGetToolVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testDeleteToolVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } - - func testUpdateToolDescription() async throws { - print("Needs Implementation") - XCTFail("Not implemented") - } } From f307ead409840591d260f540ce7859c5fdfa860b Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 17:33:45 -0700 Subject: [PATCH 37/73] Prompts working --- ...umeAI.APISpecification.RequestBodies.swift | 12 +- .../API/HumeAI.APISpecification.swift | 26 ++--- ...eAI.APISpeicification.ResponseBodies.swift | 9 +- .../Intramodular/HumeAI.Client-Prompts.swift | 75 ++++++++++++- .../Intramodular/Models/HumeAI.Prompt.swift | 15 ++- Tests/HumeAI/Intramodular/Prompts.swift | 104 +++++++++++++----- 6 files changed, 177 insertions(+), 64 deletions(-) diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index 445ad0e1..c49a67b0 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -177,16 +177,14 @@ extension HumeAI.APISpecification { struct CreatePromptInput: Codable { let name: String - let description: String? - let content: String - let metadata: [String: String]? + let text: String + let versionDescription: String? } struct CreatePromptVersionInput: Codable { let id: String - let description: String? - let content: String - let metadata: [String: String]? + let text: String + let versionDescription: String? } struct UpdatePromptNameInput: Codable { @@ -196,7 +194,7 @@ extension HumeAI.APISpecification { struct UpdatePromptDescriptionInput: Codable { let id: String - let versionID: String + let version: Int let description: String } diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index 75c848db..f0b2883e 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -126,13 +126,13 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions" + "/v0/evi/prompts/\(context.input.id)" }) - var listPromptVersions = Endpoint() + var listPromptVersions = Endpoint() @POST @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions" + "/v0/evi/prompts/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createPromptVersion = Endpoint() @@ -148,26 +148,26 @@ extension HumeAI { "/v0/evi/prompts/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updatePromptName = Endpoint() + var updatePromptName = Endpoint() @GET @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.version)" + "/v0/evi/prompts/\(context.input.id)/version/\(context.input.version)" }) var getPromptVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.version)" + "/v0/evi/prompts/\(context.input.id)/version/\(context.input.version)" }) var deletePromptVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/evi/prompts/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/prompts/\(context.input.id)/version/\(context.input.version)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updatePromptDescription = Endpoint() + var updatePromptDescription = Endpoint() // MARK: - Custom Voices @GET @@ -243,19 +243,19 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions/\(context.input.version)" + "/v0/evi/configs/\(context.input.id)/version/\(context.input.version)" }) var getConfigVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions/\(context.input.version)" + "/v0/evi/configs/\(context.input.id)/version/\(context.input.version)" }) var deleteConfigVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions/\(context.input.versionID)" + "/v0/evi/configs/\(context.input.id)/version/\(context.input.versionID)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateConfigDescription = Endpoint() @@ -435,13 +435,13 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/registry/models/\(context.input.id)/versions/\(context.input.version)" + "/v0/registry/models/\(context.input.id)/version/\(context.input.version)" }) var getModelVersion = Endpoint() @PATCH @Path({ context -> String in - "/v0/registry/models/\(context.input.id)/versions/\(context.input.versionId)" + "/v0/registry/models/\(context.input.id)/version/\(context.input.versionId)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var updateModelDescription = Endpoint() diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index 44f12a81..f55a6e2c 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -256,14 +256,7 @@ extension HumeAI.APISpecification { let pageNumber: Int let pageSize: Int let totalPages: Int - let prompts: [HumeAI.Prompt] - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case totalPages = "total_pages" - case prompts = "prompts_page" - } + let promptsPage: [HumeAI.Prompt] } struct ToolVersionList: Codable { diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift index 77c742ae..b4179ef2 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Prompts.swift @@ -12,19 +12,18 @@ import Merge extension HumeAI.Client { public func listPrompts() async throws -> [HumeAI.Prompt] { let response = try await run(\.listPrompts) - return response.prompts + return response.promptsPage } public func createPrompt( name: String, - content: String, + text: String, description: String? = nil ) async throws -> HumeAI.Prompt { let input = HumeAI.APISpecification.RequestBodies.CreatePromptInput( name: name, - description: description, - content: content, - metadata: nil + text: text, + versionDescription: description ) return try await run(\.createPrompt, with: input) } @@ -35,4 +34,70 @@ extension HumeAI.Client { ) try await run(\.deletePrompt, with: input) } + + public func listPromptVersions(id: String) async throws -> [HumeAI.Prompt] { + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.listPromptVersions, with: input).promptsPage + } + + public func createPromptVersion( + id: String, + text: String, + versionDescription: String? = nil + ) async throws -> HumeAI.Prompt.PromptVersion { + let input = HumeAI.APISpecification.RequestBodies.CreatePromptVersionInput( + id: id, + text: text, + versionDescription: versionDescription + ) + return try await run(\.createPromptVersion, with: input) + } + + public func getPromptVersion( + id: String, + version: Int + ) async throws -> HumeAI.Prompt.PromptVersion { + let input = HumeAI.APISpecification.PathInput.IDWithVersion( + id: id, + version: version + ) + return try await run(\.getPromptVersion, with: input) + } + + public func deletePromptVersion( + id: String, + version: Int + ) async throws { + let input = HumeAI.APISpecification.PathInput.IDWithVersion( + id: id, + version: version + ) + try await run(\.deletePromptVersion, with: input) + } + + public func updatePromptName( + id: String, + name: String + ) async throws { + let input = HumeAI.APISpecification.RequestBodies.UpdatePromptNameInput( + id: id, + name: name + ) + try await run(\.updatePromptName, with: input) + } + + public func updatePromptDescription( + id: String, + version: Int, + description: String + ) async throws { + let input = HumeAI.APISpecification.RequestBodies.UpdatePromptDescriptionInput( + id: id, + version: version, + description: description + ) + return try await run(\.updatePromptDescription, with: input) + } } diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift index 362e4852..e3cd0dcd 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift @@ -8,20 +8,23 @@ extension HumeAI { public struct Prompt: Codable { public let id: String + public let version: Int + public let versionType: String public let name: String - public let description: String? public let createdOn: Int64 public let modifiedOn: Int64 - public let versions: [PromptVersion]? + public let text: String + public let versionDescription: String? public struct PromptVersion: Codable { public let id: String - public let promptId: String - public let description: String? + public let version: Int + public let versionType: String + public let name: String public let createdOn: Int64 public let modifiedOn: Int64 - public let content: String - public let metadata: [String: String]? + public let text: String + public let versionDescription: String? } } } diff --git a/Tests/HumeAI/Intramodular/Prompts.swift b/Tests/HumeAI/Intramodular/Prompts.swift index c7907ce5..374a199f 100644 --- a/Tests/HumeAI/Intramodular/Prompts.swift +++ b/Tests/HumeAI/Intramodular/Prompts.swift @@ -9,52 +9,106 @@ import XCTest @testable import HumeAI final class HumeAIClientPromptTests: XCTestCase { - + func testListPrompts() async throws { let prompts = try await client.listPrompts() + + for prompt in prompts { + try await client.deletePrompt(id: prompt.id) + } + XCTAssertNotNil(prompts) } func testCreatePrompt() async throws { - let prompt = try await client.createPrompt( - name: "Test Prompt", - content: "Test Content", - description: "Test Description" - ) - XCTAssertEqual(prompt.name, "Test Prompt") - } - - func testDeletePrompt() async throws { - try await client.deletePrompt(id: "test-id") + let prompt = try await createPrompt() + try await client.deletePrompt(id: prompt.id) + + XCTAssertNotNil(prompt.id) + XCTAssertEqual(prompt.version, 0) } func testListPromptVersions() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let prompt = try await createPromptVersion() + let versions = try await client.listPromptVersions(id: prompt.prompt.id) + print("VERSIONS", versions) + try await client.deletePrompt(id: prompt.prompt.id) + XCTAssertNotNil(versions) } - func testCreatePromptVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + func testGetPromptVersion() async throws { + let prompt = try await createPromptVersion() + let promptVersion = try await client.getPromptVersion( + id: prompt.prompt.id, + version: 0 + ) + try await client.deletePrompt(id: prompt.prompt.id) + XCTAssertNotNil(promptVersion) } - func testGetPromptVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + func testCreatePromptVersion() async throws { + let prompt = try await createPrompt() + + let promptVersion = try await client.createPromptVersion( + id: prompt.id, + text: "Test Content", + versionDescription: "" + ) + + XCTAssertNotNil(promptVersion.id) + XCTAssertEqual(promptVersion.version, 0) } func testDeletePromptVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let prompt = try await createPromptVersion() + try await client.deletePromptVersion( + id: prompt.prompt.id, + version: prompt.promptVersion.version + ) } func testUpdatePromptName() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let prompt = try await createPromptVersion() + try await client.updatePromptName( + id: prompt.prompt.id, + name: "Updated name" + ) + try await client.deletePrompt(id: prompt.prompt.id) } func testUpdatePromptDescription() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let promptVersion = try await createPromptVersion() + try await client.updatePromptDescription( + id: promptVersion.prompt.id, + version: promptVersion.promptVersion.version, + description: "Updated Description" + ) + let prompts = try await client.listPrompts() + try await client.deletePrompt(id: promptVersion.prompt.id) + } + + func testDeletePrompt() async throws { + let prompt = try await createPrompt() + try await client.deletePrompt(id: prompt.id) + } + + func createPrompt() async throws -> HumeAI.Prompt { + let prompt = try await client.createPrompt( + name: "Test Prompt", + text: "Test Content" + ) + + return prompt + } + + func createPromptVersion() async throws -> (prompt: HumeAI.Prompt, promptVersion: HumeAI.Prompt.PromptVersion) { + let prompt = try await createPrompt() + + let promptVersion = try await client.createPromptVersion( + id: prompt.id, + text: "Test Content" + ) + + return (prompt, promptVersion) } } From 34fe8044d4adb94b2c7376a1cfc231095625b517 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Mon, 25 Nov 2024 18:37:30 -0700 Subject: [PATCH 38/73] Updated Tests --- ...umeAI.APISpecification.RequestBodies.swift | 6 +- .../API/HumeAI.APISpecification.swift | 42 ++++---- ...eAI.APISpeicification.ResponseBodies.swift | 13 +-- .../Intramodular/HumeAI.Client-Batch.swift | 2 +- .../HumeAI.Client-CustomVoices.swift | 47 +++++++-- .../Intramodular/HumeAI.Client-Models.swift | 43 ++++++-- .../Intramodular/HumeAI.Client-Stream.swift | 2 +- .../HumeAI/Intramodular/HumeAI.Client.swift | 2 +- .../HumeAI/Intramodular/HumeAI.Model.swift | 6 +- .../Intramodular/Models/HumeAI.Models.swift | 98 +++++++++++++++++++ Tests/HumeAI/Intramodular/Model.swift | 38 +++++-- Tests/HumeAI/Intramodular/Prompts.swift | 4 +- Tests/HumeAI/Intramodular/Voices.swift | 94 ++++++++++++++---- 13 files changed, 314 insertions(+), 83 deletions(-) create mode 100644 Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift index c49a67b0..bdc8e2bc 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.RequestBodies.swift @@ -56,7 +56,7 @@ extension HumeAI.APISpecification { struct BatchInferenceJobInput: Codable { let urls: [URL] - let models: HumeAI.Model + let models: HumeAI.APIModel let callback: CallbackConfig? } @@ -92,6 +92,7 @@ extension HumeAI.APISpecification { struct CreateConfigVersionInput: Codable { let id: String + let version: Int let description: String? let settings: [String: String] } @@ -115,6 +116,7 @@ extension HumeAI.APISpecification { struct CreateDatasetVersionInput: Codable { let id: String + let version: Int let description: String? let fileIds: [String] } @@ -201,7 +203,7 @@ extension HumeAI.APISpecification { struct StreamInput: Codable, HTTPRequest.Multipart.ContentConvertible { let id: String // Add file ID let file: Data - let models: [HumeAI.Model] + let models: [HumeAI.APIModel] let metadata: [String: String]? public func __conversion() throws -> HTTPRequest.Multipart.Content { diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index f0b2883e..b78e11a9 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -171,39 +171,39 @@ extension HumeAI { // MARK: - Custom Voices @GET - @Path("/v0/evi/custom-voices") + @Path("/v0/evi/custom_voices") var listCustomVoices = Endpoint() @POST - @Path("/v0/evi/custom-voices") + @Path("/v0/evi/custom_voices") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var createCustomVoice = Endpoint() + var createCustomVoice = Endpoint() @GET @Path({ context -> String in - "/v0/evi/custom-voices/\(context.input.id)" + "/v0/evi/custom_voices/\(context.input.id)" }) - var getCustomVoice = Endpoint() + var getCustomVoice = Endpoint() @POST @Path({ context -> String in - "/v0/evi/custom-voices/\(context.input.id)/versions" + "/v0/evi/custom_voices/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var createCustomVoiceVersion = Endpoint() + var createCustomVoiceVersion = Endpoint() @DELETE @Path({ context -> String in - "/v0/evi/custom-voices/\(context.input.id)" + "/v0/evi/custom_voices/\(context.input.id)" }) var deleteCustomVoice = Endpoint() @PATCH @Path({ context -> String in - "/v0/evi/custom-voices/\(context.input.id)" + "/v0/evi/custom_voices/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updateCustomVoiceName = Endpoint() + var updateCustomVoiceName = Endpoint() // MARK: - Configs @GET @@ -217,13 +217,13 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions" + "/v0/evi/configs/\(context.input.id)/version/\(context.input.version)" }) - var listConfigVersions = Endpoint() + var listConfigVersions = Endpoint() @POST @Path({ context -> String in - "/v0/evi/configs/\(context.input.id)/versions" + "/v0/evi/configs/\(context.input.id)/version/\(context.input.version)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createConfigVersion = Endpoint() @@ -393,7 +393,7 @@ extension HumeAI { @POST @Path({ context -> String in - "/v0/registry/datasets/\(context.input.id)/versions" + "/v0/registry/datasets/\(context.input.id)/version/\(context.input.version)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) var createDatasetVersion = Endpoint() @@ -406,9 +406,9 @@ extension HumeAI { @GET @Path({ context -> String in - "/v0/registry/datasets/\(context.input.id)/versions" + "/v0/registry/datasets/\(context.input.id)/version/\(context.input.version)" }) - var listDatasetVersions = Endpoint() + var listDatasetVersions = Endpoint() // MARK: - Models @GET @Path("/v0/registry/models") @@ -425,26 +425,26 @@ extension HumeAI { "/v0/registry/models/\(context.input.id)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updateModelName = Endpoint() + var updateModelName = Endpoint() @GET @Path({ context -> String in - "/v0/registry/models/\(context.input.id)/versions" + "/v0/registry/models/version" }) - var listModelVersions = Endpoint() + var listModelVersions = Endpoint() @GET @Path({ context -> String in "/v0/registry/models/\(context.input.id)/version/\(context.input.version)" }) - var getModelVersion = Endpoint() + var getModelVersion = Endpoint() @PATCH @Path({ context -> String in "/v0/registry/models/\(context.input.id)/version/\(context.input.versionId)" }) @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var updateModelDescription = Endpoint() + var updateModelDescription = Endpoint() // MARK: - Jobs @POST diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift index f55a6e2c..ae4a636a 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpeicification.ResponseBodies.swift @@ -24,9 +24,7 @@ extension HumeAI.APISpecification { case voices = "custom_voices_page" } } - - typealias Voice = HumeAI.Voice - + struct TTSOutput: Codable { public let audio: Data public let durationMs: Int @@ -182,14 +180,7 @@ extension HumeAI.APISpecification { let pageNumber: Int let pageSize: Int let totalPages: Int - let voices: [Voice] - - private enum CodingKeys: String, CodingKey { - case pageNumber = "page_number" - case pageSize = "page_size" - case totalPages = "total_pages" - case voices = "voices_page" - } + let customVoicesPage: [HumeAI.Voice] } struct DatasetList: Codable { diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift index 97dca70d..e034fd80 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Batch.swift @@ -12,7 +12,7 @@ import Merge extension HumeAI.Client { public func startInferenceJob( urls: [URL], - models: HumeAI.Model + models: HumeAI.APIModel ) async throws -> HumeAI.JobID { let input = HumeAI.APISpecification.RequestBodies.BatchInferenceJobInput( urls: urls, diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift index caadc511..0445d8eb 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-CustomVoices.swift @@ -12,28 +12,61 @@ import Merge extension HumeAI.Client { public func listCustomVoices() async throws -> [HumeAI.Voice] { let response = try await run(\.listCustomVoices) - return response.voices + return response.customVoicesPage } public func createCustomVoice( name: String, baseVoice: String, - model: HumeAI.Model, - parameters: HumeAI.Voice.Parameters + parameterModel: String, + parameters: HumeAI.Voice.Parameters? = nil ) async throws -> HumeAI.Voice { let input = HumeAI.APISpecification.RequestBodies.CreateVoiceInput( name: name, baseVoice: baseVoice, - parameterModel: HumeAI.paramaterModel, + parameterModel: parameterModel, parameters: parameters ) return try await run(\.createCustomVoice, with: input) } - public func deleteCustomVoice(id: String) async throws { - let input = HumeAI.APISpecification.PathInput.ID( - id: id + public func getCustomVoice( + id: String + ) async throws -> HumeAI.Voice { + let input = HumeAI.APISpecification.PathInput.ID(id: id) + return try await run(\.getCustomVoice, with: input) + } + + public func createCustomVoiceVersion( + id: String, + baseVoice: String, + parameterModel: String, + parameters: HumeAI.Voice.Parameters? = nil + ) async throws -> HumeAI.Voice { + let input = HumeAI.APISpecification.RequestBodies.CreateVoiceVersionInput( + id: id, + baseVoice: baseVoice, + parameterModel: parameterModel, + parameters: parameters ) + return try await run(\.createCustomVoiceVersion, with: input) + } + + public func deleteCustomVoice( + id: String + ) async throws { + let input = HumeAI.APISpecification.PathInput.ID(id: id) try await run(\.deleteCustomVoice, with: input) } + + public func updateCustomVoiceName( + id: String, + name: String + ) async throws { + let input = HumeAI.APISpecification.RequestBodies.UpdateVoiceNameInput( + id: id, + name: name + ) + try await run(\.updateCustomVoiceName, with: input) + } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift index e34714ec..1645c603 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Models.swift @@ -9,8 +9,6 @@ import NetworkKit import SwiftAPI import Merge -//FIXME: - Not correct Model structure - extension HumeAI.Client { public func listModels() async throws -> [HumeAI.Model] { let response = try await run(\.listModels) @@ -20,20 +18,51 @@ extension HumeAI.Client { public func getModel( id: String ) async throws -> HumeAI.Model { - let input = HumeAI.APISpecification.PathInput.ID( - id: id - ) + let input = HumeAI.APISpecification.PathInput.ID(id: id) return try await run(\.getModel, with: input) } public func updateModelName( id: String, name: String - ) async throws -> HumeAI.Model { + ) async throws { let input = HumeAI.APISpecification.RequestBodies.UpdateModelNameInput( id: id, name: name ) - return try await run(\.updateModelName, with: input) + try await run(\.updateModelName, with: input) + } + + public func listModelVersions( + id: String + ) async throws -> [HumeAI.Model.ModelVersion] { + let input = HumeAI.APISpecification.PathInput.ID( + id: id + ) + return try await run(\.listModelVersions, with: input) + } + + public func getModelVersion( + id: String, + version: Int + ) async throws -> HumeAI.Model.ModelVersion { + let input = HumeAI.APISpecification.PathInput.IDWithVersion( + id: id, + version: version + ) + return try await run(\.getModelVersion, with: input) + } + + public func updateModelDescription( + id: String, + versionId: String, + description: String + ) async throws { + let input = HumeAI.APISpecification.RequestBodies.UpdateModelDescriptionInput( + id: id, + versionId: versionId, + description: description + ) + try await run(\.updateModelDescription, with: input) } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift index 6eddd27c..bda66bbf 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client-Stream.swift @@ -13,7 +13,7 @@ extension HumeAI.Client { public func streamInference( id: String, file: Data, - models: [HumeAI.Model], + models: [HumeAI.APIModel], metadata: [String: String]? = nil ) async throws -> HumeAI.Job { let input = HumeAI.APISpecification.RequestBodies.StreamInput( diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client.swift b/Sources/HumeAI/Intramodular/HumeAI.Client.swift index 5471ef16..1e5c61a6 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Client.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Client.swift @@ -45,7 +45,7 @@ extension HumeAI.Client { // Text to Speech public func getAllAvailableVoices() async throws -> [HumeAI.Voice] { let response = try await run(\.listCustomVoices) - return response.voices + return response.customVoicesPage } } diff --git a/Sources/HumeAI/Intramodular/HumeAI.Model.swift b/Sources/HumeAI/Intramodular/HumeAI.Model.swift index c8723ca9..a97ee40d 100644 --- a/Sources/HumeAI/Intramodular/HumeAI.Model.swift +++ b/Sources/HumeAI/Intramodular/HumeAI.Model.swift @@ -11,11 +11,11 @@ import Foundation import Swift extension HumeAI { - static let paramaterModel = "20241004-11parameter" + static let parameterModel = "20241004-11parameter" } extension HumeAI { - public struct Model: Codable { + public struct APIModel: Codable { public var face: Face? public var burst: [String: String]? public var prosody: Prosody? @@ -123,7 +123,7 @@ extension HumeAI { } // Helper initializers for simpler model creation -extension HumeAI.Model { +extension HumeAI.APIModel { public static func face( fpsPred: Double? = 3.0, probThreshold: Double? = 0.99, diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift new file mode 100644 index 00000000..307c30f4 --- /dev/null +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift @@ -0,0 +1,98 @@ +// +// HumeAI.Models.swift +// AI +// +// Created by Jared Davidson on 11/25/24. +// + +extension HumeAI { + public struct Model: Codable { + public let id: String + public let name: String + public let createdOn: Int64 + public let modifiedOn: Int64 + public let totalStars: Int64 + public let modelIsStarredByUser: Bool + public let archived: Bool + public let isPubliclyShared: Bool + public let latestVersion: ModelVersion? + + public struct ModelVersion: Codable { + public let id: String + public let modelId: String + public let userId: String + public let version: String + public let sourceUri: String + public let datasetVersionId: String + public let createdOn: Int64 + public let metadata: [String: MetadataValue]? + public let description: String? + public let tags: [Tag]? + public let fileType: String? + public let targetFeature: String? + public let taskType: String? + public let trainingJobId: String? + + public struct Tag: Codable { + public let key: String + public let value: String + } + + public enum MetadataValue: Codable { + case string(String) + case object([String: String]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let objectValue = try? container.decode([String: String].self) { + self = .object(objectValue) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid metadata value") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .object(let value): + try container.encode(value) + } + } + } + } + } + + public struct ModelList: Codable { + public let content: [Model] + public let pageable: PageInfo + public let total: Int64 + public let last: Bool + public let totalElements: Int64 + public let totalPages: Int + public let size: Int + public let number: Int + public let sort: SortInfo + public let first: Bool + public let numberOfElements: Int + public let empty: Bool + + public struct PageInfo: Codable { + public let offset: Int64 + public let sort: SortInfo + public let paged: Bool + public let unpaged: Bool + public let pageNumber: Int + public let pageSize: Int + } + + public struct SortInfo: Codable { + public let empty: Bool + public let sorted: Bool + public let unsorted: Bool + } + } +} diff --git a/Tests/HumeAI/Intramodular/Model.swift b/Tests/HumeAI/Intramodular/Model.swift index 81cbca75..8c54e3d8 100644 --- a/Tests/HumeAI/Intramodular/Model.swift +++ b/Tests/HumeAI/Intramodular/Model.swift @@ -13,30 +13,52 @@ final class HumeAIClientModelTests: XCTestCase { func testListModels() async throws { let models = try await client.listModels() XCTAssertNotNil(models) + + if let model = models.first { + XCTAssertNotNil(model.id) + XCTAssertNotNil(model.name) + XCTAssertNotNil(model.latestVersion) + } } func testGetModel() async throws { let model = try await client.getModel(id: "test-id") XCTAssertNotNil(model) + XCTAssertNotNil(model.latestVersion) } func testUpdateModelName() async throws { - let model = try await client.updateModelName(id: "test-id", name: "Updated Name") - XCTAssertNotNil(model) + let model = try await client.updateModelName( + id: "test-id", + name: "Updated Name" + ) } func testListModelVersions() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let versions = try await client.listModelVersions(id: "test-id") + XCTAssertNotNil(versions) + + if let version = versions.first { + XCTAssertNotNil(version.id) + XCTAssertNotNil(version.modelId) + XCTAssertNotNil(version.version) + } } func testGetModelVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let version = try await client.getModelVersion( + id: "test-id", + version: 1 + ) + XCTAssertNotNil(version) + XCTAssertNotNil(version.modelId) } func testUpdateModelDescription() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let version = try await client.updateModelDescription( + id: "test-id", + versionId: "version-id", + description: "Updated Description" + ) } } diff --git a/Tests/HumeAI/Intramodular/Prompts.swift b/Tests/HumeAI/Intramodular/Prompts.swift index 374a199f..a7157c9e 100644 --- a/Tests/HumeAI/Intramodular/Prompts.swift +++ b/Tests/HumeAI/Intramodular/Prompts.swift @@ -23,9 +23,7 @@ final class HumeAIClientPromptTests: XCTestCase { func testCreatePrompt() async throws { let prompt = try await createPrompt() try await client.deletePrompt(id: prompt.id) - - XCTAssertNotNil(prompt.id) - XCTAssertEqual(prompt.version, 0) + XCTAssertNotNil(prompt) } func testListPromptVersions() async throws { diff --git a/Tests/HumeAI/Intramodular/Voices.swift b/Tests/HumeAI/Intramodular/Voices.swift index d0f0c70a..f7af6e71 100644 --- a/Tests/HumeAI/Intramodular/Voices.swift +++ b/Tests/HumeAI/Intramodular/Voices.swift @@ -8,39 +8,97 @@ import XCTest @testable import HumeAI -final class HumeAIClientCustomVoiceTests: XCTestCase { +final class HumeAIClientVoiceTests: XCTestCase { func testListCustomVoices() async throws { let voices = try await client.listCustomVoices() + + for voice in voices { + try await client.deleteCustomVoice(id: voice.id) + } + XCTAssertNotNil(voices) } func testCreateCustomVoice() async throws { - let voice = try await client.createCustomVoice( - name: "Test Voice", - baseVoice: "base-voice", - model: HumeAI.Model.prosody(), - parameters: .init() - ) - XCTAssertEqual(voice.name, "Test Voice") - } - - func testDeleteCustomVoice() async throws { - try await client.deleteCustomVoice(id: "test-id") + let voice = try await createVoice() + try await client.deleteCustomVoice(id: voice.id) + + XCTAssertNotNil(voice.id) + XCTAssertEqual(voice.name, "TEST VOICE") + XCTAssertEqual(voice.baseVoice, "ITO") } func testGetCustomVoice() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let voice = try await createVoice() + let retrievedVoice = try await client.getCustomVoice(id: voice.id) + try await client.deleteCustomVoice(id: voice.id) + + XCTAssertEqual(retrievedVoice.id, voice.id) } func testCreateCustomVoiceVersion() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let voiceVersion = try await createCustomVoiceVersion() + + try await client.deleteCustomVoice(id: voiceVersion.id) + + XCTAssertEqual(voiceVersion.baseVoice, "DACHER") + } + + func testDeleteCustomVoice() async throws { + let voice = try await createVoice() + try await client.deleteCustomVoice(id: voice.id) } func testUpdateCustomVoiceName() async throws { - print("Needs Implementation") - XCTFail("Not implemented") + let voice = try await createVoice() + + let updatedVoice = try await client.updateCustomVoiceName( + id: voice.id, + name: "Updated Voice Name" + ) + + try await client.deleteCustomVoice(id: voice.id) + } + + func createCustomVoiceVersion() async throws -> HumeAI.Voice { + let voice = try await client.createCustomVoice( + name: "Test Voice 2", + baseVoice: "ITO", + parameterModel: HumeAI.parameterModel, + parameters: createTestParameters() + ) + + return try await client.createCustomVoiceVersion( + id: voice.id, + baseVoice: "DACHER", + parameterModel: HumeAI.parameterModel + ) + } + + // Helper Methods + func createVoice() async throws -> HumeAI.Voice { + return try await client.createCustomVoice( + name: "Test Voice", + baseVoice: "ITO", + parameterModel: HumeAI.parameterModel, + parameters: createTestParameters() + ) + } + + func createTestParameters() -> HumeAI.Voice.Parameters { + return HumeAI.Voice.Parameters( + gender: 0.5, + articulation: 0.5, + assertiveness: 0.5, + buoyancy: 0.5, + confidence: 0.5, + enthusiasm: 0.5, + nasality: 0.5, + relaxedness: 0.5, + smoothness: 0.5, + tepidity: 0.5, + tightness: 0.5 + ) } } From dd577da5bff279a24fa81ab933bf32f9322a6a6e Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 26 Nov 2024 09:47:26 -0700 Subject: [PATCH 39/73] updated Rime --- Sources/Rime/Intramodular/Rime.Client.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Rime/Intramodular/Rime.Client.swift b/Sources/Rime/Intramodular/Rime.Client.swift index 3d007a59..ac16e931 100644 --- a/Sources/Rime/Intramodular/Rime.Client.swift +++ b/Sources/Rime/Intramodular/Rime.Client.swift @@ -95,7 +95,7 @@ extension Rime.Client { let input = Rime.APISpecification.RequestBodies.TextToSpeechInput( speaker: voice, text: text, - modelId: model.rawValue + modelID: model.rawValue ) switch outputAudio { From f6e0f6584978bf7ebade604892b54aa648a773fa Mon Sep 17 00:00:00 2001 From: NatashaTheRobot Date: Fri, 29 Nov 2024 14:36:49 +0530 Subject: [PATCH 40/73] WIP --- Package.resolved | 14 +- Package.swift | 28 ++ .../ModelIdentifier.Provider.swift | 11 + .../Service/_MIServiceTypeIdentifier.swift | 1 + .../Intramodular/ElevenLabs.Model.swift | 4 +- .../xAI.APISpecification.RequestBodies.swift | 144 ++++++++++ .../xAI.APISpecification.ResponseBodies.swift | 6 + .../API/xAI.APISpecification.swift | 120 ++++++++ .../Models/xAI.ChatCompletion.swift | 36 +++ .../Models/xAI.ChatFunctionDefinition.swift | 17 ++ .../Intramodular/Models/xAI.ChatMessage.swift | 210 ++++++++++++++ .../Models/xAI.ChatMessageBody.swift | 215 +++++++++++++++ .../Intramodular/Models/xAI.ChatRole.swift | 41 +++ .../xAI/Intramodular/Models/xAI.Tool.swift | 51 ++++ .../Intramodular/xAI+LLMRequestHandling.swift | 156 +++++++++++ .../xAI.ChatMessage+LargeLanguageModels.swift | 258 ++++++++++++++++++ Sources/xAI/Intramodular/xAI.Client.swift | 50 ++++ Sources/xAI/Intramodular/xAI.Model.swift | 52 ++++ Sources/xAI/Intramodular/xAI.swift | 7 + Sources/xAI/module.swift | 5 + .../Intramodular/EmbeddingsTests.swift | 1 - Tests/xAI/Intramodular/CompletionTests.swift | 43 +++ .../Intramodular/FunctionCallingTests.swift | 103 +++++++ Tests/xAI/module.swift | 12 + 24 files changed, 1575 insertions(+), 10 deletions(-) create mode 100644 Sources/xAI/Intramodular/API/xAI.APISpecification.RequestBodies.swift create mode 100644 Sources/xAI/Intramodular/API/xAI.APISpecification.ResponseBodies.swift create mode 100644 Sources/xAI/Intramodular/API/xAI.APISpecification.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.ChatCompletion.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.ChatFunctionDefinition.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.ChatMessage.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.ChatMessageBody.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.ChatRole.swift create mode 100644 Sources/xAI/Intramodular/Models/xAI.Tool.swift create mode 100644 Sources/xAI/Intramodular/xAI+LLMRequestHandling.swift create mode 100644 Sources/xAI/Intramodular/xAI.ChatMessage+LargeLanguageModels.swift create mode 100644 Sources/xAI/Intramodular/xAI.Client.swift create mode 100644 Sources/xAI/Intramodular/xAI.Model.swift create mode 100644 Sources/xAI/Intramodular/xAI.swift create mode 100644 Sources/xAI/module.swift create mode 100644 Tests/xAI/Intramodular/CompletionTests.swift create mode 100644 Tests/xAI/Intramodular/FunctionCallingTests.swift create mode 100644 Tests/xAI/module.swift diff --git a/Package.resolved b/Package.resolved index e257e27a..666d3ccf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "864ef9201dffd6ebf57da3ab4413cc1001edb70cd0c6d264b91e19b3967fb7ba", + "originHash" : "0db351194242c55d1647132cde67f92244bdfb0f30f99916a91c9464f714a06e", "pins" : [ { "identity" : "corepersistence", @@ -7,7 +7,7 @@ "location" : "https://github.com/vmanot/CorePersistence.git", "state" : { "branch" : "main", - "revision" : "38fd5271fa906a2d8395e4b42724142886a3c763" + "revision" : "e5026e82410d4140aa5468fe98069e4f5c4fa5cf" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/vmanot/Merge.git", "state" : { "branch" : "master", - "revision" : "e8bc37c8dc203cab481efedd71237c151882c007" + "revision" : "2a47b62831164bea212a1616cfe3d4da32902f7f" } }, { @@ -34,7 +34,7 @@ "location" : "https://github.com/vmanot/Swallow.git", "state" : { "branch" : "master", - "revision" : "6227a1114e341daf54e90df61e173599b187a9b1" + "revision" : "f91811c49863ed506da4f3aed21f6c216a258ca0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -70,7 +70,7 @@ "location" : "https://github.com/SwiftUIX/SwiftUIX.git", "state" : { "branch" : "master", - "revision" : "836fc284a9bb07fc9ab6d2dce6ebd0e32aabde26" + "revision" : "275be4fd06b570b71fe1eb47b4246f4c4285b177" } } ], diff --git a/Package.swift b/Package.swift index e4abb826..244517eb 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let package = Package( "ElevenLabs", "_Gemini", "Groq", + "xAI", "HuggingFace", "Jina", "Mistral", @@ -150,6 +151,21 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .target( + name: "xAI", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/xAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .target( name: "Ollama", dependencies: [ @@ -275,6 +291,7 @@ let package = Package( "Mistral", "_Gemini", "Groq", + "xAI", "Ollama", "OpenAI", "Perplexity", @@ -345,6 +362,17 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .testTarget( + name: "xAITests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/xAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .testTarget( name: "PerplexityTests", dependencies: [ diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index fbdc91cc..0ec80659 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -15,6 +15,7 @@ extension ModelIdentifier { case _Fal case _Mistral case _Groq + case _xAI case _Ollama case _OpenAI case _Gemini @@ -47,6 +48,10 @@ extension ModelIdentifier { Self._Groq } + public static var xAI: Self { + Self._xAI + } + public static var gemini: Self { Self._Gemini } @@ -92,6 +97,8 @@ extension ModelIdentifier.Provider: CustomStringConvertible { return "Mistral" case ._Groq: return "Groq" + case ._xAI: + return "xAI" case ._Ollama: return "Ollama" case ._OpenAI: @@ -129,6 +136,8 @@ extension ModelIdentifier.Provider: RawRepresentable { return "mistral" case ._Groq: return "groq" + case ._xAI: + return "xai" case ._Ollama: return "ollama" case ._OpenAI: @@ -164,6 +173,8 @@ extension ModelIdentifier.Provider: RawRepresentable { self = ._Mistral case Self._Groq.rawValue: self = ._Groq + case Self._xAI.rawValue: + self = ._xAI case Self._OpenAI.rawValue: self = ._OpenAI case Self._Gemini.rawValue: diff --git a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift index 00fe0f99..413465c0 100644 --- a/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift +++ b/Sources/CoreMI/Intramodular/Service/_MIServiceTypeIdentifier.swift @@ -38,4 +38,5 @@ extension _MIServiceTypeIdentifier { public static let _VoyageAI = _MIServiceTypeIdentifier(rawValue: "hajat-fufoh-janaf-disam") public static let _Cohere = _MIServiceTypeIdentifier(rawValue: "guzob-fipin-navij-duvon") public static let _TogetherAI = _MIServiceTypeIdentifier(rawValue: "pafob-vopoj-lurig-zilur") + public static let _xAI = _MIServiceTypeIdentifier(rawValue: "niluj-futol-guhaj-pabas") } diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Model.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Model.swift index 6d181027..35f1b09a 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Model.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Model.swift @@ -33,7 +33,7 @@ extension ElevenLabs.Model: CustomStringConvertible { extension ElevenLabs.Model: ModelIdentifierRepresentable { public init(from identifier: ModelIdentifier) throws { - guard identifier.provider == ._Groq, identifier.revision == nil else { + guard identifier.provider == ._ElevenLabs, identifier.revision == nil else { throw Never.Reason.illegal } @@ -46,7 +46,7 @@ extension ElevenLabs.Model: ModelIdentifierRepresentable { public func __conversion() -> ModelIdentifier { ModelIdentifier( - provider: ._Groq, + provider: ._ElevenLabs, name: rawValue, revision: nil ) diff --git a/Sources/xAI/Intramodular/API/xAI.APISpecification.RequestBodies.swift b/Sources/xAI/Intramodular/API/xAI.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..1753bfaf --- /dev/null +++ b/Sources/xAI/Intramodular/API/xAI.APISpecification.RequestBodies.swift @@ -0,0 +1,144 @@ + + +import Foundation + +extension xAI.APISpecification.RequestBodies { + + /* https://docs.x.ai/api/endpoints#chat-completions */ + struct ChatCompletions: Codable, Hashable, Sendable { + private enum CodingKeys: String, CodingKey { + case model + case messages + case temperature + case topProbabilityMass = "top_p" + case choices = "n" + case stream + case stop + case maxTokens = "max_tokens" + case presencePenalty = "presence_penalty" + case frequencyPenalty = "frequency_penalty" + case logprobs = "logprobs" + case logitBias = "logit_bias" + case seed = "seed" + case topLogprobs = "top_logprobs" + case user + case tools + } + + /* Model name for the model to use. */ + var model: xAI.Model + + /* A list of messages that make up the the chat conversation. Different models support different message types, such as image and text.*/ + var messages: [xAI.ChatMessage] + + /* What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.*/ + var temperature: Double? + + /* An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. It is generally recommended to alter this or `temperature` but not both.*/ + var topProbabilityMass: Double? + + /*The maximum number of tokens that can be generated in the chat completion. This value can be used to control costs for text generated via API.*/ + var maxTokens: Int? + + /* If set, partial message deltas will be sent. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a `data: [DONE]` message.*/ + var stream: Bool? + + /* If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result. Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend.*/ + var seed: Int? + + /* Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. */ + var frequencyPenalty: Double? + + /* A JSON object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.*/ + var logitBias: [String: Int]? + + /* Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities of each output token returned in the content of message. */ + var logprobs: Bool? + + /* How many chat completion choices to generate for each input message. Note that you will be charged based on the number of generated tokens across all of the choices. Keep n as 1 to minimize costs. */ + var choices: Int? + + /* Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. */ + var presencePenalty: Double? + + /* Up to 4 sequences where the API will stop generating further tokens. */ + var stop: [String]? + + /* An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. logprobs must be set to true if this parameter is used. */ + var topLogprobs: Int? + + /* A unique identifier representing your end-user, which can help xAI to monitor and detect abuse. */ + var user: String? + + /* A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. */ + var tools: [xAI.Tool]? + + init( + messages: [xAI.ChatMessage], + model: xAI.Model, + frequencyPenalty: Double? = nil, + logitBias: [String : Int]? = nil, + logprobs: Bool? = nil, + topLogprobs: Int? = nil, + maxTokens: Int? = nil, + choices: Int? = nil, + presencePenalty: Double? = nil, + seed: Int? = nil, + stop: [String]? = nil, + stream: Bool? = nil, + temperature: Double? = nil, + topProbabilityMass: Double? = nil, + user: String? = nil, + functions: [xAI.ChatFunctionDefinition]? = nil + ) { + self.messages = messages + self.model = model + self.frequencyPenalty = frequencyPenalty + self.logitBias = logitBias + self.logprobs = logprobs + self.topLogprobs = topLogprobs + self.maxTokens = maxTokens + self.choices = choices + self.presencePenalty = presencePenalty + self.seed = seed + self.stop = stop + self.stream = stream + self.temperature = temperature + self.topProbabilityMass = topProbabilityMass + self.user = user + self.tools = functions?.map { xAI.Tool.function($0) } + + } + + init( + user: String?, + messages: [xAI.ChatMessage], + model: xAI.Model, + temperature: Double?, + topProbabilityMass: Double?, + choices: Int?, + stream: Bool?, + stop: [String]?, + maxTokens: Int?, + presencePenalty: Double?, + frequencyPenalty: Double? + ) { + self.user = user + self.messages = messages + self.model = model + self.temperature = temperature + self.topProbabilityMass = topProbabilityMass + self.choices = choices + self.stream = stream + self.stop = stop + self.maxTokens = maxTokens + self.presencePenalty = presencePenalty + self.frequencyPenalty = frequencyPenalty + + self.logitBias = nil + self.logprobs = nil + self.topLogprobs = nil + self.seed = nil + } + } +} diff --git a/Sources/xAI/Intramodular/API/xAI.APISpecification.ResponseBodies.swift b/Sources/xAI/Intramodular/API/xAI.APISpecification.ResponseBodies.swift new file mode 100644 index 00000000..6ca529f0 --- /dev/null +++ b/Sources/xAI/Intramodular/API/xAI.APISpecification.ResponseBodies.swift @@ -0,0 +1,6 @@ + +import Foundation + +extension xAI.APISpecification.ResponseBodies { + +} diff --git a/Sources/xAI/Intramodular/API/xAI.APISpecification.swift b/Sources/xAI/Intramodular/API/xAI.APISpecification.swift new file mode 100644 index 00000000..89be500e --- /dev/null +++ b/Sources/xAI/Intramodular/API/xAI.APISpecification.swift @@ -0,0 +1,120 @@ + + +import NetworkKit +import FoundationX +import Swallow + +extension xAI { + public enum APIError: APIErrorProtocol { + public typealias API = xAI.APISpecification + + case apiKeyMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case badRequest(request: API.Request?, error: API.Request.Error) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var apiKey: String? + } + + public let configuration: Configuration + + public var host: URL { + URL(string: "https://api.x.ai/v1")! + } + + public var id: some Hashable { + configuration + } + + @POST + @Path("chat/completions") + var chatCompletions = Endpoint() + } +} + +extension xAI.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + let configuration = context.root.configuration + + return try super + .buildRequestBase(from: input, context: context) + .jsonBody(input, keyEncodingStrategy: .convertToSnakeCase) + .header(.contentType(.json)) + .header(.accept(.json)) + .header(.authorization(.bearer, configuration.apiKey.unwrap())) + } + + struct _ErrorWrapper: Codable, Hashable, Sendable { + struct Error: Codable, Hashable, Sendable { + let type: String + let param: AnyCodable? + let message: String + } + + let error: Error + } + + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? Request.Error { + if let error = try? response.decode( + _ErrorWrapper.self, + keyDecodingStrategy: .convertFromSnakeCase + ).error { + if error.message.contains("You didn't provide an API key") { + throw Error.apiKeyMissing + } else if error.message.contains("Incorrect API key provided") { + throw Error.incorrectAPIKeyProvided + } + } + + if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} + +extension xAI.APISpecification { + public enum RequestBodies: _StaticSwift.Namespace { + + } + + public enum ResponseBodies: _StaticSwift.Namespace { + + } +} diff --git a/Sources/xAI/Intramodular/Models/xAI.ChatCompletion.swift b/Sources/xAI/Intramodular/Models/xAI.ChatCompletion.swift new file mode 100644 index 00000000..084aebdc --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.ChatCompletion.swift @@ -0,0 +1,36 @@ + + +import Foundation + +extension xAI { + public struct ChatCompletion: Codable, Hashable, Sendable { + + public struct Choice: Codable, Hashable, Sendable { + public enum FinishReason: String, Codable, Hashable, Sendable { + case stop = "stop" + case length = "length" + case modelLength = "model_length" + case toolCalls = "tool_calls" + } + + public let index: Int + public let message: ChatMessage + public let finishReason: FinishReason + } + + public struct Usage: Codable, Hashable, Sendable { + public let promptTokens: Int + public let completionTokens: Int + public let totalTokens: Int + } + + public var id: String + public var object: String + public var created: Date + public var model: Model + public var choices: [Choice] + public let usage: Usage + public let systemFingerprint: String + } +} + diff --git a/Sources/xAI/Intramodular/Models/xAI.ChatFunctionDefinition.swift b/Sources/xAI/Intramodular/Models/xAI.ChatFunctionDefinition.swift new file mode 100644 index 00000000..ca4b8169 --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.ChatFunctionDefinition.swift @@ -0,0 +1,17 @@ + +import CorePersistence + +extension xAI { + public struct ChatFunctionDefinition: Codable, Hashable, Sendable { + public let name: String + public let description: String + public let parameters: JSONSchema + + public init(name: String, description: String, parameters: JSONSchema) { + self.name = name + self.description = description + self.parameters = parameters + } + } +} + diff --git a/Sources/xAI/Intramodular/Models/xAI.ChatMessage.swift b/Sources/xAI/Intramodular/Models/xAI.ChatMessage.swift new file mode 100644 index 00000000..13b3f651 --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.ChatMessage.swift @@ -0,0 +1,210 @@ + +import CorePersistence +import Diagnostics +import LargeLanguageModels +import Swallow + +extension xAI { + public struct ChatMessage: Hashable, Sendable { + public typealias ID = String + + public let id: ID + public let role: ChatRole + public var body: ChatMessageBody + + public init( + id: ID? = nil, + role: ChatRole, + body: ChatMessageBody + ) { + switch body { + case .text: + assert(role != .function) + case .content: + assert(role != .function) + case .functionCall: + assert(role == .assistant) + case .toolCalls(_): + assert(role == .assistant) + case .functionInvocation: + assert(role == .function) + } + + self.id = id ?? UUID().stringValue // FIXME: !!! + self.role = role + self.body = body + } + } + + public enum FunctionCallingStrategy: Codable, Hashable, Sendable { + enum CodingKeys: String, CodingKey { + case none = "none" + case auto = "auto" + case function = "name" + } + + case none + case auto + case function(String) + + public init(from decoder: Decoder) throws { + switch try decoder._determineContainerKind() { + case .singleValue: + let rawValue = try decoder.singleValueContainer().decode(String.self) + + switch rawValue { + case CodingKeys.none.rawValue: + self = .none + case CodingKeys.auto.rawValue: + self = .auto + default: + throw DecodingError.dataCorrupted(.init(codingPath: [])) + } + case .keyed: + let container = try decoder.container(keyedBy: CodingKeys.self) + + self = try .function(container.decode(String.self, forKey: .function)) + default: + throw DecodingError.dataCorrupted(.init(codingPath: [])) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .none: + var container = encoder.singleValueContainer() + + try container.encode(CodingKeys.none.rawValue) + case .auto: + var container = encoder.singleValueContainer() + + try container.encode(CodingKeys.auto.rawValue) + case .function(let name): + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(name, forKey: .function) + } + } + } +} + +// MARK: - Conformances + +extension xAI.ChatMessage: AbstractLLM.ChatMessageConvertible { + public func __conversion() throws -> AbstractLLM.ChatMessage { + .init( + id: .init(rawValue: id), + role: try role.__conversion(), + content: try PromptLiteral(from: self) + ) + } +} + +extension xAI.ChatMessage: Codable { + public enum CodingKeys: CodingKey { + case id + case role + case content + case name + case functionCall + case toolCalls + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + print(try JSON(from: decoder).prettyPrintedDescription) + + + self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().stringValue // FIXME + self.role = try container.decode(xAI.ChatRole.self, forKey: .role) + + switch role { + case .function: + self.body = .functionInvocation( + .init( + name: try container.decode(String.self, forKey: .name), + response: try container.decode(String.self, forKey: .name) + ) + ) + case .assistant: + if let toolCalls = try container.decodeIfPresent([xAI.ToolCall].self, forKey: .toolCalls) { + if let function = toolCalls.first?.function { + self.body = .functionCall(function) + } else { + self.body = .toolCalls(toolCalls) + } + + } else { + self.body = try .content(container.decode(String.self, forKey: .content)) + } + default: + self.body = try .content(container.decode(String.self, forKey: .content)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(role, forKey: .role) + + switch body { + case .text(let content): + try container.encode(content, forKey: .content) + case .content(let content): + try container.encode(content, forKey: .content) + case .functionCall(let call): + try _tryAssert(role == .assistant) + + try container.encode(call, forKey: .functionCall) + try container.encodeNil(forKey: .content) + case .toolCalls(let calls): + try _tryAssert(role == .assistant) + + try container.encode(calls, forKey: .toolCalls) + try container.encodeNil(forKey: .content) + case .functionInvocation(let invocation): + try _tryAssert(role == .function) + + try container.encode(invocation.name, forKey: .name) + try container.encode(invocation.response, forKey: .content) + } + } +} + +// MARK: - Initializers + +extension xAI.ChatMessage { + public init( + id: ID? = nil, + role: xAI.ChatRole, + body: String + ) { + self.init( + id: id, + role: role, + body: .content(body) + ) + } + + public init( + role: xAI.ChatRole, + content: String + ) { + self.init( + role: role, + body: content + ) + } + + public static func system( + _ content: String + ) -> Self { + Self(id: UUID().stringValue, role: .system, body: .content(content)) + } + + public static func user( + _ content: String + ) -> Self { + Self(id: UUID().stringValue, role: .user, body: .content(content)) + } +} diff --git a/Sources/xAI/Intramodular/Models/xAI.ChatMessageBody.swift b/Sources/xAI/Intramodular/Models/xAI.ChatMessageBody.swift new file mode 100644 index 00000000..1f9122e1 --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.ChatMessageBody.swift @@ -0,0 +1,215 @@ + + +import CorePersistence +import Diagnostics +import Swift + +extension xAI { + public enum ChatMessageBody: Hashable, Sendable { + + + public struct FunctionCall: Codable, Hashable, Sendable { + public let name: String + public let arguments: String + + public init(name: String, arguments: String) { + self.name = name + self.arguments = arguments + } + } + + public struct FunctionInvocation: Codable, Hashable, Sendable { + public let name: String + public let response: String + + public init(name: String, response: String) { + self.name = name + self.response = response + } + } + + case text(String) + case content([_Content]) + /// The call made to a function provided to the LLM. + case functionCall(FunctionCall) + case toolCalls([ToolCall]) + /// The result of a function call of a function that was provided to the LLM. + case functionInvocation(FunctionInvocation) + } +} + +// MARK: - Initializers + +extension xAI.ChatMessageBody { + public static func content(_ text: String) -> Self { + .text(text) + } +} + +// MARK: - Extensions + +extension xAI.ChatMessageBody { + public var isEmpty: Bool { + switch self { + case .text(let text): + return text.isEmpty + case .content(let content): + return content.isEmpty + case .functionCall: + return false + case .toolCalls(let toolCalls): + return false + case .functionInvocation: + return false + } + } + + var _textValue: String? { + guard case .text(let string) = self else { + return nil + } + + return string + } + + public mutating func append(_ newText: String) throws { + switch self { + case .text(let text): + self = .text(text.appending(contentsOf: newText)) + case .content(let content): + self = .content(content.appending(.text(newText))) + case .functionCall: + throw Never.Reason.illegal + case .toolCalls(let toolCalls): + throw Never.Reason.illegal + case .functionInvocation: + throw Never.Reason.illegal + } + } + + public static func += (lhs: inout Self, rhs: String) throws { + try lhs.append(rhs) + } +} + +// MARK: - Auxiliary + +extension xAI.ChatMessageBody { + enum _ContentType: String, Codable, Hashable, Sendable { + case text = "text" + case imageURL = "image_url" + } + + public enum _Content: Sendable { + public struct ImageURL: Codable, Hashable, Sendable { + /// https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding + public enum ImageDetail: String, Codable, Hashable, Sendable { + case low + case high + case auto + } + + public let url: URL + public let detail: ImageDetail + + public init(url: URL, detail: ImageDetail = .auto) { + self.url = url + self.detail = detail + } + } + + case text(String) + case imageURL(ImageURL) + + public static func imageURL(_ url: URL) -> Self { + Self.imageURL(ImageURL(url: url, detail: .auto)) + } + } +} + +// MARK: - Conformances + +extension xAI.ChatMessageBody._Content: Codable { + fileprivate enum CodingKeys: String, CodingKey { + case type + case text + case imageURL = "image_url" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + print(try JSON(from: decoder).prettyPrintedDescription) + + let contentType = try container.decode(xAI.ChatMessageBody._ContentType.self, forKey: .type) + + switch contentType { + case .text: + self = .text(try container.decode(String.self, forKey: .text)) + case .imageURL: + self = .imageURL(try container.decode(ImageURL.self, forKey: .imageURL)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .text(let text): + try container.encode("text", forKey: .type) + try container.encode(text, forKey: .text) + case .imageURL(let imageURL): + try container.encode("image_url", forKey: .type) + try container.encode(imageURL, forKey: .imageURL) + } + } +} + +extension xAI.ChatMessageBody: CustomStringConvertible { + public var description: String { + switch self { + case .text(let text): + return text.description + case .content(let content): + return content.description + case .functionCall(let call): + return "\(call.name)(\(call.arguments))" + case .toolCalls(let calls): + return calls.map { "\($0.function.name)(\($0.function.arguments))" }.joined(separator: ", ") + case .functionInvocation(let invocation): + return "\(invocation.name)(...) = \(invocation.response)" + } + } +} + +extension xAI.ChatMessageBody._Content: CustomStringConvertible { + public var description: String { + switch self { + case .text(let text): + return text.description + case .imageURL(let imageURL): + return imageURL.url.description + } + } +} + +extension xAI.ChatMessageBody._Content: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case .text(let string): + hasher.combine(string) + case .imageURL(let url): + hasher.combine(url) + } + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.text(a), .text(b)): + return a == b + case let (.imageURL(a), .imageURL(b)): + return a == b + default: + return false + } + } +} + diff --git a/Sources/xAI/Intramodular/Models/xAI.ChatRole.swift b/Sources/xAI/Intramodular/Models/xAI.ChatRole.swift new file mode 100644 index 00000000..b7e04857 --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.ChatRole.swift @@ -0,0 +1,41 @@ + +import CorePersistence +import Diagnostics +import LargeLanguageModels +import Swallow + +extension xAI { + public enum ChatRole: String, Codable, Hashable, Sendable { + case system + case user + case assistant + case function + + public init(from role: AbstractLLM.ChatRole) { + switch role { + case .system: + self = .system + case .user: + self = .user + case .assistant: + self = .assistant + case .other(.function): + self = .function + } + } + + public func __conversion() throws -> AbstractLLM.ChatRole { + switch self { + case .system: + return .system + case .user: + return .user + case .assistant: + return .assistant + case .function: + return .other(.function) + } + } + } +} + diff --git a/Sources/xAI/Intramodular/Models/xAI.Tool.swift b/Sources/xAI/Intramodular/Models/xAI.Tool.swift new file mode 100644 index 00000000..148be8e4 --- /dev/null +++ b/Sources/xAI/Intramodular/Models/xAI.Tool.swift @@ -0,0 +1,51 @@ + + +extension xAI { + public enum ToolType: String, CaseIterable, Codable, Hashable, Sendable { + /* Currently, only functions are supported as a tool. */ + case function + } + + public struct Tool: Codable, Hashable, Sendable { + public let type: ToolType + public let function: xAI.ChatFunctionDefinition? + + private init( + type: ToolType, + function: xAI.ChatFunctionDefinition? + ) { + self.type = type + self.function = function + + if function != nil { + assert(type == .function) + } + } + + public static func function( + _ function: xAI.ChatFunctionDefinition + ) -> Self { + Self(type: .function, function: function) + } + } + + public struct ToolCall: Codable, Hashable, Sendable { + public let index: Int? + public let id: String? + public let type: ToolType? + public let function: ChatMessageBody.FunctionCall + + public init( + index: Int? = nil, + id: String?, + type: ToolType = .function, + function: ChatMessageBody.FunctionCall + ) { + self.index = index + self.id = id + self.type = type + self.function = function + } + } +} + diff --git a/Sources/xAI/Intramodular/xAI+LLMRequestHandling.swift b/Sources/xAI/Intramodular/xAI+LLMRequestHandling.swift new file mode 100644 index 00000000..e978f81a --- /dev/null +++ b/Sources/xAI/Intramodular/xAI+LLMRequestHandling.swift @@ -0,0 +1,156 @@ + + +import CorePersistence +import LargeLanguageModels +import NetworkKit +import Swallow + +extension xAI.Client: _TaskDependenciesExporting { + public var _exportedTaskDependencies: TaskDependencies { + var result = TaskDependencies() + + result[\.llm] = self + + return result + } +} + +extension xAI.Client: LLMRequestHandling { + public var _availableModels: [ModelIdentifier]? { + xAI.Model.allCases.map({ $0.__conversion() }) + } + + public func complete( + prompt: Prompt, + parameters: Prompt.CompletionParameters + ) async throws -> Prompt.Completion { + let _completion: Any + + switch prompt { + case let prompt as AbstractLLM.TextPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + + case let prompt as AbstractLLM.ChatPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + default: + throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) + } + + return try cast(_completion) + } + + private func _complete( + prompt: AbstractLLM.TextPrompt, + parameters: AbstractLLM.TextCompletionParameters + ) async throws -> AbstractLLM.TextCompletion { + throw LLMRequestHandlingError.unsupportedPromptType(.init(Swift.type(of: prompt))) + } + + private func _complete( + prompt: AbstractLLM.ChatPrompt, + parameters: AbstractLLM.ChatCompletionParameters + ) async throws -> AbstractLLM.ChatCompletion { + + var messages: [xAI.ChatMessage] = [] + for message in prompt.messages { + let chatMessage = try await xAI.ChatMessage(from: message) + messages.append(chatMessage) + } + + let response: xAI.ChatCompletion = try await run( + \.chatCompletions, + with: .init( + messages: messages, + model: _model(for: prompt, parameters: parameters), + maxTokens: parameters.tokenLimit?.fixedValue, + seed: nil, + stream: false, + temperature: parameters.temperatureOrTopP?.temperature, + topProbabilityMass: parameters.temperatureOrTopP?.topProbabilityMass, + functions: parameters.functions?.map { xAI.ChatFunctionDefinition(from: $0) } + ) + ) + + assert(response.choices.count == 1) + + let message = try AbstractLLM.ChatMessage(from: response, choiceIndex: 0) + + return AbstractLLM.ChatCompletion( + prompt: prompt.messages, + message: message, + stopReason: .init() // FIXME: !!! + ) + } + + private func _model( + for prompt: AbstractLLM.ChatPrompt, + parameters: AbstractLLM.ChatCompletionParameters? + ) throws -> xAI.Model { + try prompt.context.get(\.modelIdentifier)?.as(xAI.Model.self) ?? .grok_beta + } +} + +// MARK: - Auxiliary + +extension AbstractLLM.ChatRole { + public init( + from role: xAI.ChatRole + ) throws { + switch role { + case .system: + self = .system + case .user: + self = .user + case .assistant: + self = .assistant + case .function: + self = .other(.function) + } + } +} + +extension AbstractLLM.ChatMessage { + public init( + from completion: xAI.ChatCompletion, + choiceIndex: Int + ) throws { + let choice = completion.choices[choiceIndex] + + self.init( + id: AnyPersistentIdentifier(erasing: "\(completion.id)_\(choiceIndex.description)"), + role: try AbstractLLM.ChatRole(from: choice.message.role), + content: PromptLiteral(choice.message.body.description) + ) + } +} + +extension xAI.ChatMessage { + public init( + from message: AbstractLLM.ChatMessage + ) throws { + self.init( + role: xAI.ChatRole( + from: message.role + ), + content: try message.content._stripToText() + ) + } +} + +extension xAI.ChatFunctionDefinition { + public init( + from function: AbstractLLM.ChatFunctionDefinition + ) { + self.init( + name: function.name.rawValue, + description: function.context, + parameters: function.parameters + ) + } +} diff --git a/Sources/xAI/Intramodular/xAI.ChatMessage+LargeLanguageModels.swift b/Sources/xAI/Intramodular/xAI.ChatMessage+LargeLanguageModels.swift new file mode 100644 index 00000000..a1937e11 --- /dev/null +++ b/Sources/xAI/Intramodular/xAI.ChatMessage+LargeLanguageModels.swift @@ -0,0 +1,258 @@ + + +import CorePersistence +import FoundationX +@_spi(Internal) import LargeLanguageModels + +extension xAI.ChatMessage: _PromptLiteralEncodingContainer { + public mutating func encode( + _ component: PromptLiteral._Degenerate.Component + ) async throws { + var content: [xAI.ChatMessageBody._Content] + + switch self.body { + case .text(let _content): + content = [.text(_content)] + case .content(let _content): + content = _content + case .functionCall(_): + throw Never.Reason.unsupported + case .toolCalls(_): + throw Never.Reason.unsupported + case .functionInvocation(_): + throw Never.Reason.unsupported + } + + switch component.payload { + case .string(let string): + content.append(.text(string)) + case .image(let image): + let imageURL: Base64DataURL = try await image.toBase64DataURL() + + content.append(.imageURL(xAI.ChatMessageBody._Content.ImageURL(url: imageURL.url, detail: .auto))) + case .functionCall: + throw Never.Reason.unsupported + case .resultOfFunctionCall: + throw Never.Reason.unsupported + } + + self = .init( + id: nil, // FIXME: !!! + role: role, + body: .content(content) + ) + } +} + +extension xAI.ChatMessage { + public init( + from message: AbstractLLM.ChatMessage + ) async throws { + let role: xAI.ChatRole + + switch message.role { + case .system: + role = .system + case .user: + role = .user + case .assistant: + role = .assistant + case .other(.function): + role = .function + } + + let _content = try message.content._degenerate() + + if _content.components.contains(where: { $0.payload.type == .functionCall || $0.payload.type == .functionInvocation }) { + switch try _content.components.toCollectionOfOne().value.payload { + case .functionCall(let call): + self.init( + id: nil, + // FIXME: !!! + role: role, + body: .functionCall( + xAI.ChatMessageBody.FunctionCall( + name: call.name.rawValue, + arguments: try call.arguments.__conversion() + ) + ) + ) + case .resultOfFunctionCall(let result): + self.init( + id: nil, // FIXME: !!! + role: role, + body: .functionInvocation( + .init( + name: result.name.rawValue, + response: try result.result.__conversion() as String + ) + ) + ) + default: + assertionFailure("Unsupported prompt literal.") + + throw Never.Reason.illegal + } + } else { + var _temp = Self( + id: nil, // FIXME: !!! + role: role, + body: .content([]) + ) + + try await message.content._encode(to: &_temp) + + self = _temp + } + } +} + +extension AbstractLLM.ChatMessage { + public init( + from message: xAI.ChatMessage + ) throws { + let id = message.id + let role: AbstractLLM.ChatRole + + switch message.role { + case .system: + role = .system + case .user: + role = .user + case .assistant: + role = .assistant + case .function: + role = .other(.function) + } + + switch message.body { + case .text(let content): + self.init( + id: AnyPersistentIdentifier(erasing: id), + role: role, + content: PromptLiteral( + content, + role: .chat(role) + ) + ) + case .content(let content): + self.init( + id: AnyPersistentIdentifier(erasing: id), + role: role, + content: PromptLiteral( + from: content, + role: .chat(role) + ) + ) + case .functionCall(let call): + self.init( + id: AnyPersistentIdentifier(erasing: id), + role: role, + content: try PromptLiteral( + functionCall: .init( + functionID: nil, + name: AbstractLLM.ChatFunction.Name(rawValue: call.name), + arguments: AbstractLLM.ChatFunctionCall.Arguments(unencoded: call.arguments), + context: .init() + ), + role: .chat(role) + ) + ) + case .toolCalls(let calls): + guard let firstCall = calls.first?.function else { + throw DecodingError.dataCorrupted(.init( + codingPath: [], + debugDescription: "Tool calls array is empty" + )) + } + + self.init( + id: AnyPersistentIdentifier(erasing: id), + role: role, + content: try PromptLiteral( + functionCall: .init( + functionID: nil, + name: AbstractLLM.ChatFunction.Name(rawValue: firstCall.name), + arguments: AbstractLLM.ChatFunctionCall.Arguments(unencoded: firstCall.arguments), + context: .init() + ), + role: .chat(role) + ) + ) + case .functionInvocation(let invocation): + self.init( + id: AnyPersistentIdentifier(erasing: id), + role: role, + content: try .init( + functionInvocation: .init( + functionID: nil, + name: AbstractLLM.ChatFunction.Name(rawValue: invocation.name), + result: .init(rawValue: invocation.response) + ), + role: .chat(role) + ) + ) + } + } +} + +extension PromptLiteral { + public init(from message: xAI.ChatMessage) throws { + let role: PromptMatterRole + + switch message.role { + case .system: + role = .chat(.system) + case .user: + role = .chat(.user) + case .assistant: + role = .chat(.assistant) + case .function: + role = .chat(.other(.function)) + } + + switch message.body { + case .text(let text): + self.init(from: [.text(text)], role: role) + case .content(let content): + self.init(from: content, role: role) + case .functionCall: + TODO.unimplemented + case .toolCalls(_): + TODO.unimplemented + case .functionInvocation: + TODO.unimplemented + } + } + + init( + from contents: [xAI.ChatMessageBody._Content], + role: PromptMatterRole + ) { + var components: [PromptLiteral.StringInterpolation.Component] = [] + + for content in contents { + switch content { + case .text(let content): + components.append( + PromptLiteral.StringInterpolation.Component( + payload: .stringLiteral(content), + role: role + ) + ) + case .imageURL(let image): + assert(image.detail == .auto) // FIXME + + components.append( + PromptLiteral.StringInterpolation.Component( + payload: .image(.url(image.url)), + role: role + ) + ) + } + } + + self.init(stringInterpolation: .init(components: components)) + } +} + diff --git a/Sources/xAI/Intramodular/xAI.Client.swift b/Sources/xAI/Intramodular/xAI.Client.swift new file mode 100644 index 00000000..eb33832f --- /dev/null +++ b/Sources/xAI/Intramodular/xAI.Client.swift @@ -0,0 +1,50 @@ + + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension xAI { + @RuntimeDiscoverable + public final class Client: HTTPClient, _StaticSwift.Namespace { + public static var persistentTypeRepresentation: some IdentityRepresentation { + _MIServiceTypeIdentifier._xAI + } + + public let interface: APISpecification + public let session: HTTPSession + + public init(interface: APISpecification, session: HTTPSession) { + self.interface = interface + self.session = session + } + + public convenience init(apiKey: String?) { + self.init( + interface: .init(configuration: .init(apiKey: apiKey)), + session: .shared + ) + } + } +} + +extension xAI.Client: _MIService { + public convenience init( + account: (any _MIServiceAccount)? + ) async throws { + let account: any _MIServiceAccount = try account.unwrap() + let serviceIdentifier: _MIServiceTypeIdentifier = account.serviceIdentifier + + guard serviceIdentifier == _MIServiceTypeIdentifier._xAI else { + throw _MIServiceError.serviceTypeIncompatible(serviceIdentifier) + } + + guard let credential = account.credential as? _MIServiceAPIKeyCredential else { + throw _MIServiceError.invalidCredentials(account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} diff --git a/Sources/xAI/Intramodular/xAI.Model.swift b/Sources/xAI/Intramodular/xAI.Model.swift new file mode 100644 index 00000000..bc06398d --- /dev/null +++ b/Sources/xAI/Intramodular/xAI.Model.swift @@ -0,0 +1,52 @@ + + +import CoreMI +import CorePersistence +import LargeLanguageModels +import Swallow + +extension xAI { + public enum Model: String, CaseIterable, Codable, Hashable, Named, Sendable { + case grok_beta = "grok-beta" + case grok_vision_beta = "grok-vision-beta" + + public var name: String { + switch self { + case .grok_beta: + return "Grok Beta" + case .grok_vision_beta: + return "Grok Vision Beta" + } + } + } +} + +// MARK: - Conformances + +extension xAI.Model: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension xAI.Model: ModelIdentifierRepresentable { + public init(from identifier: ModelIdentifier) throws { + guard identifier.provider == ._xAI, identifier.revision == nil else { + throw Never.Reason.illegal + } + + guard let model = Self(rawValue: identifier.name) else { + throw Never.Reason.unexpected + } + + self = model + } + + public func __conversion() -> ModelIdentifier { + ModelIdentifier( + provider: ._xAI, + name: rawValue, + revision: nil + ) + } +} diff --git a/Sources/xAI/Intramodular/xAI.swift b/Sources/xAI/Intramodular/xAI.swift new file mode 100644 index 00000000..24ccd67a --- /dev/null +++ b/Sources/xAI/Intramodular/xAI.swift @@ -0,0 +1,7 @@ + +import Swift + +public enum xAI { + +} + diff --git a/Sources/xAI/module.swift b/Sources/xAI/module.swift new file mode 100644 index 00000000..86462ca6 --- /dev/null +++ b/Sources/xAI/module.swift @@ -0,0 +1,5 @@ + + +@_exported import LargeLanguageModels +@_exported import SwallowMacrosClient + diff --git a/Tests/Mistral/Intramodular/EmbeddingsTests.swift b/Tests/Mistral/Intramodular/EmbeddingsTests.swift index 4aee6269..3d8c2d75 100644 --- a/Tests/Mistral/Intramodular/EmbeddingsTests.swift +++ b/Tests/Mistral/Intramodular/EmbeddingsTests.swift @@ -3,7 +3,6 @@ // import LargeLanguageModels -import Groq import XCTest import Mistral diff --git a/Tests/xAI/Intramodular/CompletionTests.swift b/Tests/xAI/Intramodular/CompletionTests.swift new file mode 100644 index 00000000..ef9e78b9 --- /dev/null +++ b/Tests/xAI/Intramodular/CompletionTests.swift @@ -0,0 +1,43 @@ + + +import LargeLanguageModels +import xAI +import XCTest + +final class CompletionTests: XCTestCase { + + let llm: any LLMRequestHandling = client + + func testChatCompletionsGrokBeta() async throws { + let result = try await resultForModel(xAI.Model.grok_beta) + print(result) // "Hey! What's up with you?" + } + + func testChatCompletionsGrokVisionBeta() async throws { + let result = try await resultForModel(xAI.Model.grok_vision_beta) + print(result) // "Hey! How can I help you today?" + } + + private func resultForModel(_ model: xAI.Model) async throws -> String { + + let messages: [AbstractLLM.ChatMessage] = [ + AbstractLLM.ChatMessage( + role: .system, + body: "You are an extremely intelligent assistant." + ), + AbstractLLM.ChatMessage( + role: .user, + body: "Sup?" + ) + ] + + let result: String = try await llm.complete( + messages, + model: model, + as: String.self + ) + + return result + } +} + diff --git a/Tests/xAI/Intramodular/FunctionCallingTests.swift b/Tests/xAI/Intramodular/FunctionCallingTests.swift new file mode 100644 index 00000000..ba6af3cb --- /dev/null +++ b/Tests/xAI/Intramodular/FunctionCallingTests.swift @@ -0,0 +1,103 @@ + + +import CorePersistence +import xAI +import XCTest + +final class FunctionCallingTests: XCTestCase { + let llm: any LLMRequestHandling = client + + func testFunctionCalling() async throws { + let messages: [AbstractLLM.ChatMessage] = [ + .system { + "You are a Metereologist Expert accurately giving weather data in fahrenheit at any given city around the world" + }, + .user { + "What is the weather in San Francisco, CA?" + } + ] + + let functionCall1: AbstractLLM.ChatFunctionCall = try await llm.complete( + messages, + functions: [makeGetWeatherFunction1()], + as: .functionCall + ) + + let functionCall2: AbstractLLM.ChatFunctionCall = try await llm.complete( + messages, + functions: [makeGetWeatherFunction2()], + as: .functionCall + ) + + let result1 = try functionCall1.decode(GetWeatherParameters.self) + let result2 = try functionCall2.decode(GetWeatherParameters.self) + + print(result1, result2) + } + + private func makeGetWeatherFunction1() -> AbstractLLM.ChatFunctionDefinition { + let weatherObjectSchema = JSONSchema( + type: .object, + description: "Weather in a certain location", + properties: [ + "location": JSONSchema( + type: .string, + description: "The city and state, e.g. San Francisco, CA" + ), + "unit_fahrenheit" : JSONSchema( + type: .number, + description: "The unit of temperature in 'fahrenheit'" + ) + ], + required: true + ) + + let getWeatherFunction: AbstractLLM.ChatFunctionDefinition = AbstractLLM.ChatFunctionDefinition( + name: "get_weather", + context: "Get the current weather in a given location", + parameters: JSONSchema( + type: .object, + description: "Weather data for a given location in fahrenheit", + properties: [ + "weather": .array(weatherObjectSchema) + ] + ) + ) + + return getWeatherFunction + } + + struct GetWeatherParameters: Codable, Hashable, Sendable { + let weather: [WeatherObject] + } + + struct WeatherObject: Codable, Hashable, Sendable { + let location: String + let unit_fahrenheit: Double? + } + + private func makeGetWeatherFunction2() throws -> AbstractLLM.ChatFunctionDefinition { + let getWeatherFunction: AbstractLLM.ChatFunctionDefinition = AbstractLLM.ChatFunctionDefinition( + name: "get_weather", + context: "Get the current weather in a given location", + parameters: JSONSchema( + type: .object, + description: "Weather data for a given location in fahrenheit", + properties: [ + "weather": try .array { + try JSONSchema( + type: WeatherObject.self, + description: "Weather in a certain location", + propertyDescriptions: [ + "location": "The city and state, e.g. San Francisco, CA", + "unit_fahrenheit": "The unit of temperature in 'fahrenheit'" + ] + ) + } + ] + ) + ) + + return getWeatherFunction + } +} diff --git a/Tests/xAI/module.swift b/Tests/xAI/module.swift new file mode 100644 index 00000000..bd88733f --- /dev/null +++ b/Tests/xAI/module.swift @@ -0,0 +1,12 @@ + + +import xAI + +public var xAI_API_KEY: String { + "xai-iukwcbTFm3HCyuJVq7U5c0c9LKHJ0uhnGIsiOyn4Qu0zxSH3g1ULDSkaCHHoDQnX9tsV5cSCWom0HosP" +} + +public var client: xAI.Client { + xAI.Client(apiKey: xAI_API_KEY) +} + From 7e10f24cabb13927cdbcffa6b2706df4cd9f27e7 Mon Sep 17 00:00:00 2001 From: NatashaTheRobot Date: Sat, 30 Nov 2024 14:40:35 +0530 Subject: [PATCH 41/73] separated out embedding models, added completion models --- Package.resolved | 14 +- Package.swift | 11 + .../TogetherAI.APISpecification.swift | 2 +- .../Intramodular/TogetherAI.Client.swift | 2 +- .../Intramodular/TogetherAI.Model.swift | 197 +++++++++++++++--- Tests/TogetherAI/module.swift | 2 +- 6 files changed, 193 insertions(+), 35 deletions(-) diff --git a/Package.resolved b/Package.resolved index e257e27a..a3cf72c2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "864ef9201dffd6ebf57da3ab4413cc1001edb70cd0c6d264b91e19b3967fb7ba", + "originHash" : "43bc324f7aba4797f38e1f72595973383d186932faefc9a4b3698c64d5b9cb44", "pins" : [ { "identity" : "corepersistence", @@ -7,7 +7,7 @@ "location" : "https://github.com/vmanot/CorePersistence.git", "state" : { "branch" : "main", - "revision" : "38fd5271fa906a2d8395e4b42724142886a3c763" + "revision" : "cfbee4e123a18cb893613cdd536391bb7dec2203" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/vmanot/Merge.git", "state" : { "branch" : "master", - "revision" : "e8bc37c8dc203cab481efedd71237c151882c007" + "revision" : "925ca4baa33f8462d0ecd0757e7efabdac0a27ea" } }, { @@ -34,7 +34,7 @@ "location" : "https://github.com/vmanot/Swallow.git", "state" : { "branch" : "master", - "revision" : "6227a1114e341daf54e90df61e173599b187a9b1" + "revision" : "85d690b23077728eff23e9fc0297e724995e9d89" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -70,7 +70,7 @@ "location" : "https://github.com/SwiftUIX/SwiftUIX.git", "state" : { "branch" : "master", - "revision" : "836fc284a9bb07fc9ab6d2dce6ebd0e32aabde26" + "revision" : "a68663989c8aaae013c4104c6a4aa2f35afe1000" } } ], diff --git a/Package.swift b/Package.swift index f2c02ee9..b41e64ff 100644 --- a/Package.swift +++ b/Package.swift @@ -507,6 +507,17 @@ let package = Package( swiftSettings: [ .enableExperimentalFeature("AccessLevelOnImport") ] + ), + .testTarget( + name: "TogetherAITests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/TogetherAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] ) ] ) diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift index 07b41904..dd1252d2 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift @@ -122,7 +122,7 @@ extension TogetherAI.APISpecification { extension TogetherAI.APISpecification.RequestBodies { public struct CreateEmbedding: Codable, Hashable { - public let model: TogetherAI.Model + public let model: TogetherAI.Model.Embedding public let input: String } } diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift index 6601259f..4ce369db 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift @@ -53,7 +53,7 @@ extension TogetherAI.Client: CoreMI._ServiceClientProtocol { extension TogetherAI.Client { public func createEmbeddings( - for model: TogetherAI.Model, + for model: TogetherAI.Model.Embedding, input: String ) async throws -> TogetherAI.Embeddings { try await run( diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift index 316cf1d3..52ea5e62 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift @@ -7,8 +7,61 @@ import CorePersistence import LargeLanguageModels import Swallow +public protocol _TogetherAI_ModelType: Codable, Hashable, RawRepresentable, Sendable where RawValue == String { + var contextSize: Int { get throws } +} + extension TogetherAI { - public enum Model: String, CaseIterable, Codable, Hashable, Named, Sendable { + public typealias _ModelType = _TogetherAI_ModelType +} + +extension TogetherAI { + public enum Model: CaseIterable, _TogetherAI_ModelType, Hashable { + public private(set) static var allCases: [Model] = { + var result: [Model] = [] + + result += Embedding.allCases.map({ Self.embedding($0 )}) + result += Completion.allCases.map({ Self.completion($0) }) + + return result + }() + + case completion(Completion) + case embedding(Embedding) + case unknown(String) + + public var name: String { + if let base = (base as? any Named) { + return base.name.description + } else { + return base.rawValue + } + } + + private var base: any TogetherAI._ModelType { + switch self { + case .completion(let value): + return value + case .embedding(let value): + return value + case .unknown: + assertionFailure(.unimplemented) + + return self + } + } + + public var contextSize: Int { + get throws { + try base.contextSize + } + } + } +} + +extension TogetherAI.Model { + public enum Embedding: String, TogetherAI._ModelType, CaseIterable { + // Together Models case togetherM2Bert80M2KRetrieval = "togethercomputer/m2-bert-80M-2k-retrieval" case togetherM2Bert80M8KRetrieval = "togethercomputer/m2-bert-80M-8k-retrieval" @@ -29,21 +82,21 @@ extension TogetherAI { public var name: String { switch self { - case .togetherM2Bert80M2KRetrieval: + case .togetherM2Bert80M2KRetrieval: return "M2-BERT-80M-2K-Retrieval" - case .togetherM2Bert80M8KRetrieval: + case .togetherM2Bert80M8KRetrieval: return "M2-BERT-80M-8K-Retrieval" - case .togetherM2Bert80M32KRetrieval: + case .togetherM2Bert80M32KRetrieval: return "M2-BERT-80M-32K-Retrieval" - case .whereIsAIUAELargeV1: + case .whereIsAIUAELargeV1: return "UAE-Large-v1" - case .baaiLargeENV15: + case .baaiLargeENV15: return "BGE-Large-EN-v1.5" - case .baaiBaseENV15: + case .baaiBaseENV15: return "BGE-Base-EN-v1.5" - case .sentenceBERT: + case .sentenceBERT: return "Sentence-BERT" - case .googleBERTBaseUncased: + case .googleBERTBaseUncased: return "BERT" } } @@ -72,47 +125,141 @@ extension TogetherAI { } } - public var contextWindow: Int { + public var contextSize: Int { switch self { - case .togetherM2Bert80M2KRetrieval: + case .togetherM2Bert80M2KRetrieval: return 2048 - case .togetherM2Bert80M8KRetrieval: + case .togetherM2Bert80M8KRetrieval: return 8192 - case .togetherM2Bert80M32KRetrieval: + case .togetherM2Bert80M32KRetrieval: return 32768 case .whereIsAIUAELargeV1, .baaiLargeENV15, .baaiBaseENV15, .sentenceBERT, .googleBERTBaseUncased: return 512 } } + + public init?(rawValue: String) { + switch rawValue { + case "togethercomputer/m2-bert-80M-2k-retrieval": + self = .togetherM2Bert80M2KRetrieval + case "togethercomputer/m2-bert-80M-8k-retrieval": + self = .togetherM2Bert80M8KRetrieval + case "togethercomputer/m2-bert-80M-32k-retrieval": + self = .togetherM2Bert80M32KRetrieval + case "WhereIsAI/UAE-Large-V1": + self = .whereIsAIUAELargeV1 + case "BAAI/bge-large-en-v1.5": + self = .baaiLargeENV15 + case "BAAI/bge-base-en-v1.5": + self = .baaiBaseENV15 + case "sentence-transformers/msmarco-bert-base-dot-v5": + self = .sentenceBERT + case "bert-base-uncased": + self = .googleBERTBaseUncased + default: + return nil + } + } + } +} + +extension TogetherAI.Model { + public enum Completion: String, TogetherAI._ModelType, CaseIterable { + + case metaLlama2_70B = "meta-llama/Llama-2-70b-hf" + case mistral7b = "mistralai/Mistral-7B-v0.1" + case mixtral8x7b = "mistralai/Mixtral-8x7B-v0.1" + + public var name: String { + switch self { + case .metaLlama2_70B: + return "Meta LLaMA-2 (70B)" + case .mistral7b: + return "Mistral (7B)" + case .mixtral8x7b: + return "Mixtral-8x7B (46.7B)" + } + } + + public var contextSize: Int { + switch self { + case .metaLlama2_70B: + return 4096 + case .mistral7b: + return 8192 + case .mixtral8x7b: + return 32768 + } + } + + public init?(rawValue: String) { + switch rawValue { + case "meta-llama/Llama-2-70b-hf": + self = .metaLlama2_70B + case "mistralai/Mistral-7B-v0.1": + self = .mistral7b + case "mistralai/Mixtral-8x7B-v0.1": + self = .mixtral8x7b + default: + return nil + } + } } } // MARK: - Conformances -extension TogetherAI.Model: CustomStringConvertible { - public var description: String { - rawValue +extension TogetherAI.Model: Codable { + public init(from decoder: Decoder) throws { + self = try Self(rawValue: try String(from: decoder)).unwrap() + } + + public func encode(to encoder: Encoder) throws { + try rawValue.encode(to: encoder) } } extension TogetherAI.Model: ModelIdentifierRepresentable { - public init(from identifier: ModelIdentifier) throws { - guard identifier.provider == ._TogetherAI, identifier.revision == nil else { - throw Never.Reason.illegal - } - - guard let model = Self(rawValue: identifier.name) else { - throw Never.Reason.unexpected + private enum _DecodingError: Error { + case invalidModelProvider + } + + public init(from model: ModelIdentifier) throws { + guard model.provider == .openAI else { + throw _DecodingError.invalidModelProvider } - self = model + self = try Self(rawValue: model.name).unwrap() } public func __conversion() -> ModelIdentifier { ModelIdentifier( - provider: ._TogetherAI, + provider: .togetherAI, name: rawValue, revision: nil ) } } + +extension TogetherAI.Model: RawRepresentable { + public var rawValue: String { + switch self { + case .completion(let model): + return model.rawValue + case .embedding(let model): + return model.rawValue + case .unknown(let rawValue): + return rawValue + } + } + + public init?(rawValue: String) { + if let model = Completion(rawValue: rawValue) { + self = .completion(model) + } else if let model = Embedding(rawValue: rawValue) { + self = .embedding(model) + } else { + self = .unknown(rawValue) + } + } +} diff --git a/Tests/TogetherAI/module.swift b/Tests/TogetherAI/module.swift index 67d8ba23..7d2491eb 100644 --- a/Tests/TogetherAI/module.swift +++ b/Tests/TogetherAI/module.swift @@ -5,7 +5,7 @@ import TogetherAI public var TOGETHERAI_API_KEY: String { - "" + "e7a0f4590185d90881baded9ca54b0f173838b8b001e4450c0759ab9894c949b" } public var client: TogetherAI.Client { From 2a121f17ba9f7ef45ca3e658a0097de441916dbb Mon Sep 17 00:00:00 2001 From: NatashaTheRobot Date: Sat, 30 Nov 2024 15:46:55 +0530 Subject: [PATCH 42/73] added completions api to togetherai --- .../TogetherAI.APISpecification.swift | 90 +++++++++++++++++++ .../Intramodular/TogetherAI.Client.swift | 35 ++++++++ .../Intramodular/TogetherAI.Completion.swift | 41 +++++++++ .../Intramodular/TogetherAI.Model.swift | 10 +-- .../Intramodular/CompetionTests.swift | 49 ++++++++++ Tests/TogetherAI/module.swift | 2 +- 6 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 Sources/TogetherAI/Intramodular/TogetherAI.Completion.swift create mode 100644 Tests/TogetherAI/Intramodular/CompetionTests.swift diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift index dd1252d2..97dd0e9e 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift @@ -41,6 +41,10 @@ extension TogetherAI { @POST @Path("embeddings") public var createEmbeddings = Endpoint() + + @POST + @Path("completions") + public var createCompletion = Endpoint() } } @@ -125,4 +129,90 @@ extension TogetherAI.APISpecification.RequestBodies { public let model: TogetherAI.Model.Embedding public let input: String } + + public struct CreateCompletion: Codable, Hashable { + + private enum CodingKeys: String, CodingKey { + case model + case prompt + case maxTokens = "max_tokens" + case stream + case stop + case temperature + case topP = "top_p" + case topK = "top_k" + case repetitionPenalty = "repetition_penalty" + case logprobs + case echo + case choices = "n" + case safetyModel = "safety_model" + } + + public let model: TogetherAI.Model.Completion + public let prompt: String + + // The maximum number of tokens to generate. + // Defaults to 200 + public let maxTokens: Int? + + // If true, stream tokens as Server-Sent Events as the model generates them instead of waiting for the full model response. If false, return a single JSON object containing the results. + public let stream: Bool? + + // A list of string sequences that will truncate (stop) inference text output. For example, "" will stop generation as soon as the model generates the given token. + public let stop: [String]? + + // A decimal number that determines the degree of randomness in the response. A value of 1 will always yield the same output. A temperature less than 1 favors more correctness and is appropriate for question answering or summarization. A value greater than 1 introduces more randomness in the output. + public let temperature: Double? + + // The top_p (nucleus) parameter is used to dynamically adjust the number of choices for each predicted token based on the cumulative probabilities. It specifies a probability threshold, below which all less likely tokens are filtered out. This technique helps to maintain diversity and generate more fluent and natural-sounding text. + public let topP: Double? + + // The top_k parameter is used to limit the number of choices for the next predicted word or token. It specifies the maximum number of tokens to consider at each step, based on their probability of occurrence. This technique helps to speed up the generation process and can improve the quality of the generated text by focusing on the most likely options. + public let topK: Double? + + // A number that controls the diversity of generated text by reducing the likelihood of repeated sequences. Higher values decrease repetition. + public let repetitionPenalty: Double? + + // Number of top-k logprobs to return + public let logprobs: Int? + + // Echo prompt in output. Can be used with logprobs to return prompt logprobs. + public let echo: Bool? + + // How many completions to generate for each prompt + public let choices: Int? + + // A moderation model to validate tokens. Choice between available moderation models found here: https://docs.together.ai/docs/inference-models#moderation-models + public let safetyModel: String? + + public init( + model: TogetherAI.Model.Completion, + prompt: String, + maxTokens: Int?, + stream: Bool? = nil, + stop: [String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Double? = nil, + repetitionPenalty: Double? = nil, + logprobs: Int? = nil, + echo: Bool? = nil, + choices: Int? = nil, + safetyModel: String? = nil + ) { + self.model = model + self.prompt = prompt + self.maxTokens = maxTokens ?? 200 + self.stream = stream + self.stop = stop + self.temperature = temperature + self.topP = topP + self.topK = topK + self.repetitionPenalty = repetitionPenalty + self.logprobs = logprobs + self.echo = echo + self.choices = choices + self.safetyModel = safetyModel + } + } } diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift index 4ce369db..ef90e5ee 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift @@ -64,4 +64,39 @@ extension TogetherAI.Client { ) ) } + + public func createCompletion( + for model: TogetherAI.Model.Completion, + prompt: String, + maxTokens: Int? = nil, + stream: Bool? = nil, + stop: [String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Double? = nil, + repetitionPenalty: Double? = nil, + logprobs: Int? = nil, + echo: Bool? = nil, + choices: Int? = nil, + safetyModel: String? = nil + ) async throws -> TogetherAI.Completion { + try await run( + \.createCompletion, + with: .init( + model: model, + prompt: prompt, + maxTokens: maxTokens, + stream: stream, + stop: stop, + temperature: temperature, + topP: topP, + topK: topK, + repetitionPenalty: repetitionPenalty, + logprobs: logprobs, + echo: echo, + choices: choices, + safetyModel: safetyModel + ) + ) + } } diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Completion.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Completion.swift new file mode 100644 index 00000000..e65c4391 --- /dev/null +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Completion.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) Vatsal Manot +// + +import NetworkKit +import Swift + +extension TogetherAI { + public final class Completion: Codable, Sendable { + private enum CodingKeys: String, CodingKey { + case id + case object + case model + case createdAt = "created" + case choices + case usage + } + + public struct Choice: Codable, Hashable, Sendable { + public let text: String + public let index: Int + public let seed: Double + public let finishReason: String + } + + public struct Usage: Codable, Hashable, Sendable { + public let promptTokens: Int + public let completionTokens: Int + public let totalTokens: Int + } + + + public let id: String + public let model: TogetherAI.Model.Completion + public let object: String + public let createdAt: Date + public let choices: [Choice] + public let usage: Usage + } +} + diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift index 52ea5e62..8a487824 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift @@ -166,14 +166,14 @@ extension TogetherAI.Model { extension TogetherAI.Model { public enum Completion: String, TogetherAI._ModelType, CaseIterable { - case metaLlama2_70B = "meta-llama/Llama-2-70b-hf" + case llama2_70B = "meta-llama/Llama-2-70b-hf" case mistral7b = "mistralai/Mistral-7B-v0.1" case mixtral8x7b = "mistralai/Mixtral-8x7B-v0.1" public var name: String { switch self { - case .metaLlama2_70B: - return "Meta LLaMA-2 (70B)" + case .llama2_70B: + return "LLaMA-2 (70B)" case .mistral7b: return "Mistral (7B)" case .mixtral8x7b: @@ -183,7 +183,7 @@ extension TogetherAI.Model { public var contextSize: Int { switch self { - case .metaLlama2_70B: + case .llama2_70B: return 4096 case .mistral7b: return 8192 @@ -195,7 +195,7 @@ extension TogetherAI.Model { public init?(rawValue: String) { switch rawValue { case "meta-llama/Llama-2-70b-hf": - self = .metaLlama2_70B + self = .llama2_70B case "mistralai/Mistral-7B-v0.1": self = .mistral7b case "mistralai/Mixtral-8x7B-v0.1": diff --git a/Tests/TogetherAI/Intramodular/CompetionTests.swift b/Tests/TogetherAI/Intramodular/CompetionTests.swift new file mode 100644 index 00000000..9febc433 --- /dev/null +++ b/Tests/TogetherAI/Intramodular/CompetionTests.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) Vatsal Manot +// + +import Foundation +import LargeLanguageModels +import TogetherAI +import XCTest + +final class CompletionTests: XCTestCase { + + func testCompletionsLlama() async throws { + let completion = try await client + .createCompletion( + for: .llama2_70B, + prompt: "List all of the states in the USA and their capitals in a table.", + maxTokens: 200, + temperature: 0.7, + choices: 5 + ) + print(completion) + } + + func testCompletionsMistral() async throws { + let completion = try await client + .createCompletion( + for: .mistral7b, + prompt: "List all of the states in the USA and their capitals in a table.", + maxTokens: 400, + temperature: 0.5, + choices: 2 + ) + print(completion) + } + + func testCompletionsMixtral() async throws { + let completion = try await client + .createCompletion( + for: .mixtral8x7b, + prompt: "List all of the states in the USA and their capitals in a table.", + maxTokens: 175, + temperature: 0.9, + choices: 3 + ) + print(completion) + } +} + + diff --git a/Tests/TogetherAI/module.swift b/Tests/TogetherAI/module.swift index 7d2491eb..e856c439 100644 --- a/Tests/TogetherAI/module.swift +++ b/Tests/TogetherAI/module.swift @@ -5,7 +5,7 @@ import TogetherAI public var TOGETHERAI_API_KEY: String { - "e7a0f4590185d90881baded9ca54b0f173838b8b001e4450c0759ab9894c949b" + "YOUR_API_KEY" } public var client: TogetherAI.Client { From 9aae12c2133b7b1c09ccd8cfac5b5ecedb1e5617 Mon Sep 17 00:00:00 2001 From: NatashaTheRobot Date: Sat, 30 Nov 2024 16:25:09 +0530 Subject: [PATCH 43/73] added llmrequesthandling --- ...TogetherAI.Client+LLMRequestHandling.swift | 128 ++++++++++++++++++ .../Intramodular/TogetherAI.Model.swift | 2 +- .../Intramodular/CompetionTests.swift | 20 ++- 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift new file mode 100644 index 00000000..12c4d3fc --- /dev/null +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift @@ -0,0 +1,128 @@ +// +// Copyright (c) Vatsal Manot +// + +import CoreMI +import CorePersistence +import Diagnostics +@_spi(Internal) import LargeLanguageModels +import Merge +import Swallow + +extension TogetherAI.Client: _TaskDependenciesExporting { + public var _exportedTaskDependencies: TaskDependencies { + var result = TaskDependencies() + + result[\.llm] = self + //result[\.embedding] = self + + return result + } +} + +extension TogetherAI.Client: LLMRequestHandling { + private var _debugPrintCompletions: Bool { + false + } + + public var _availableModels: [ModelIdentifier]? { + return TogetherAI.Model.allCases.map({ $0.__conversion() }) + } + + public func complete( + prompt: Prompt, + parameters: Prompt.CompletionParameters + ) async throws -> Prompt.Completion { + let _completion: Any + + switch prompt { + case let prompt as AbstractLLM.TextPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + default: + throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) + } + + return try cast(_completion) + } + + private func _complete( + prompt: AbstractLLM.TextPrompt, + parameters: AbstractLLM.TextCompletionParameters + ) async throws -> AbstractLLM.TextCompletion { + let parameters = try cast(parameters, to: AbstractLLM.TextCompletionParameters.self) + + let model = TogetherAI.Model.Completion.mixtral8x7b + + let promptText = try prompt.prefix.promptLiteral + let completion = try await + self.createCompletion( + for: model, + prompt: promptText._stripToText(), + maxTokens: parameters.tokenLimit.fixedValue, + stop: parameters.stops, + temperature: parameters.temperatureOrTopP?.temperature, + topP: parameters.temperatureOrTopP?.topProbabilityMass + ) + + let text = try completion.choices.toCollectionOfOne().first.text + + _debugPrint( + prompt: prompt.debugDescription + .delimited(by: .quotationMark) + .delimited(by: "\n") + , + completion: text + .delimited(by: .quotationMark) + .delimited(by: "\n") + ) + + + return .init(prefix: promptText, text: text) + } +} + +extension TogetherAI.Client { + private func _debugPrint(prompt: String, completion: String) { + guard _debugPrintCompletions else { + return + } + + guard _isDebugAssertConfiguration else { + return + } + + let description = String.concatenate(separator: "\n") { + "=== [PROMPT START] ===" + prompt.debugDescription + .delimited(by: .quotationMark) + .delimited(by: "\n") + "==== [COMPLETION] ====" + completion + .delimited(by: .quotationMark) + .delimited(by: "\n") + "==== [PROMPT END] ====" + } + + print(description) + } +} + +// MARK: - Auxiliary + +extension ModelIdentifier { + + public init( + from model: TogetherAI.Model.Completion + ) { + self.init(provider: .togetherAI, name: model.rawValue, revision: nil) + } + + public init( + from model: TogetherAI.Model.Embedding + ) { + self.init(provider: .togetherAI, name: model.rawValue, revision: nil) + } +} diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift index 8a487824..e12b71af 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Model.swift @@ -225,7 +225,7 @@ extension TogetherAI.Model: ModelIdentifierRepresentable { } public init(from model: ModelIdentifier) throws { - guard model.provider == .openAI else { + guard model.provider == .togetherAI else { throw _DecodingError.invalidModelProvider } diff --git a/Tests/TogetherAI/Intramodular/CompetionTests.swift b/Tests/TogetherAI/Intramodular/CompetionTests.swift index 9febc433..0cb98147 100644 --- a/Tests/TogetherAI/Intramodular/CompetionTests.swift +++ b/Tests/TogetherAI/Intramodular/CompetionTests.swift @@ -8,7 +8,25 @@ import TogetherAI import XCTest final class CompletionTests: XCTestCase { - + + func testCompletionsLLMRequest() async throws { + let llm: any LLMRequestHandling = client + + let prompt: AbstractLLM.TextPrompt = .init(stringLiteral: "List all of the states in the USA and their capitals in a table.") + let parameters: AbstractLLM.TextCompletionParameters = .init( + tokenLimit: .fixed(400), + temperature: 0.8, + stops: nil + ) + + let completion = try await llm.complete( + prompt: prompt, + parameters: parameters + ) + + print(completion) + } + func testCompletionsLlama() async throws { let completion = try await client .createCompletion( From 4d410c05a95256e680975845ef7a6139ea01caf1 Mon Sep 17 00:00:00 2001 From: NatashaTheRobot Date: Sat, 30 Nov 2024 16:50:38 +0530 Subject: [PATCH 44/73] added text embeddings request handling conformance --- .../TogetherAI.APISpecification.swift | 2 +- ...TogetherAI.Client+LLMRequestHandling.swift | 2 +- ...Client+TextEmbeddingsRequestHandling.swift | 40 +++++++++++++++++++ .../Intramodular/TogetherAI.Client.swift | 15 ++++++- .../Intramodular/TogetherAI.Embeddings.swift | 2 +- .../Intramodular/EmbeddingsTests.swift | 22 +++++++++- 6 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 Sources/TogetherAI/Intramodular/TogetherAI.Client+TextEmbeddingsRequestHandling.swift diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift index 97dd0e9e..0416b7a8 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.APISpecification.swift @@ -127,7 +127,7 @@ extension TogetherAI.APISpecification { extension TogetherAI.APISpecification.RequestBodies { public struct CreateEmbedding: Codable, Hashable { public let model: TogetherAI.Model.Embedding - public let input: String + public let input: [String] } public struct CreateCompletion: Codable, Hashable { diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift index 12c4d3fc..27526ffa 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client+LLMRequestHandling.swift @@ -14,7 +14,7 @@ extension TogetherAI.Client: _TaskDependenciesExporting { var result = TaskDependencies() result[\.llm] = self - //result[\.embedding] = self + result[\.embedding] = self return result } diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client+TextEmbeddingsRequestHandling.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client+TextEmbeddingsRequestHandling.swift new file mode 100644 index 00000000..e7c6e816 --- /dev/null +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client+TextEmbeddingsRequestHandling.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) Vatsal Manot +// + +import CoreMI +import CorePersistence + +extension TogetherAI.Client: TextEmbeddingsRequestHandling { + public func fulfill( + _ request: TextEmbeddingsRequest + ) async throws -> TextEmbeddings { + guard !request.input.isEmpty else { + return TextEmbeddings( + model: .init(from: TogetherAI.Model.Embedding.togetherM2Bert80M2KRetrieval), + data: [] + ) + } + + let model: ModelIdentifier = request.model ?? ModelIdentifier(from: TogetherAI.Model.Embedding.togetherM2Bert80M2KRetrieval) + let embeddingModel = try TogetherAI.Model.Embedding(rawValue: model.name).unwrap() + + let embeddings = try await createEmbeddings( + for: embeddingModel, + input: request.input + ).data + + try _tryAssert(request.input.count == embeddings.count) + + return TextEmbeddings( + model: .init(from: TogetherAI.Model.Embedding.togetherM2Bert80M2KRetrieval), + data: request.input.zip(embeddings).map { + TextEmbeddings.Element( + text: $0, + embedding: $1.embedding, + model: model + ) + } + ) + } +} diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift index ef90e5ee..5948d898 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Client.swift @@ -54,7 +54,7 @@ extension TogetherAI.Client: CoreMI._ServiceClientProtocol { extension TogetherAI.Client { public func createEmbeddings( for model: TogetherAI.Model.Embedding, - input: String + input: [String] ) async throws -> TogetherAI.Embeddings { try await run( \.createEmbeddings, @@ -65,6 +65,19 @@ extension TogetherAI.Client { ) } + public func createEmbeddings( + for model: TogetherAI.Model.Embedding, + input: String + ) async throws -> TogetherAI.Embeddings { + try await run( + \.createEmbeddings, + with: .init( + model: model, + input: [input] + ) + ) + } + public func createCompletion( for model: TogetherAI.Model.Completion, prompt: String, diff --git a/Sources/TogetherAI/Intramodular/TogetherAI.Embeddings.swift b/Sources/TogetherAI/Intramodular/TogetherAI.Embeddings.swift index a30e8576..02464834 100644 --- a/Sources/TogetherAI/Intramodular/TogetherAI.Embeddings.swift +++ b/Sources/TogetherAI/Intramodular/TogetherAI.Embeddings.swift @@ -15,7 +15,7 @@ extension TogetherAI { extension TogetherAI.Embeddings { public struct EmbeddingData: Codable, Hashable, Sendable { public let object: String - public let embedding: [Float] + public let embedding: [Double] public let index: Int } } diff --git a/Tests/TogetherAI/Intramodular/EmbeddingsTests.swift b/Tests/TogetherAI/Intramodular/EmbeddingsTests.swift index a9de0f2b..ea5a3b69 100644 --- a/Tests/TogetherAI/Intramodular/EmbeddingsTests.swift +++ b/Tests/TogetherAI/Intramodular/EmbeddingsTests.swift @@ -3,11 +3,31 @@ // import LargeLanguageModels -import VoyageAI +import TogetherAI import XCTest final class EmbeddingsTests: XCTestCase { + func testTextEmbeddingsRequesntHandling() async { + let textEmbeddingsClient: any TextEmbeddingsRequestHandling = client + let textInput = "Our solar system orbits the Milky Way galaxy at about 515,000 mph" + + do { + let embeddings = try await textEmbeddingsClient.fulfill( + .init( + input: [textInput], + model: ModelIdentifier( + from: TogetherAI.Model.Embedding.togetherM2Bert80M8KRetrieval + ) + ) + ) + let embeddingsData = embeddings.data + XCTAssertTrue(!embeddingsData.isEmpty) + } catch { + XCTFail(String(describing: error)) + } + } + func testTextEmbeddings() async { let textInput = "Our solar system orbits the Milky Way galaxy at about 515,000 mph" do { From 3cf471a7d2ac758da71a246912bf30aebe19fa88 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 3 Dec 2024 09:59:32 -0700 Subject: [PATCH 45/73] CreateImageEdit --- Package.resolved | 14 +++--- ...penAI.APISpecification.RequestBodies.swift | 44 +++++++++++++++++++ .../Intramodular/Models/OpenAI.Model.swift | 3 ++ .../Intramodular/OpenAI.Client-Image.swift | 8 ++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/Package.resolved b/Package.resolved index e257e27a..941f0af2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "864ef9201dffd6ebf57da3ab4413cc1001edb70cd0c6d264b91e19b3967fb7ba", + "originHash" : "43bc324f7aba4797f38e1f72595973383d186932faefc9a4b3698c64d5b9cb44", "pins" : [ { "identity" : "corepersistence", @@ -7,7 +7,7 @@ "location" : "https://github.com/vmanot/CorePersistence.git", "state" : { "branch" : "main", - "revision" : "38fd5271fa906a2d8395e4b42724142886a3c763" + "revision" : "e5026e82410d4140aa5468fe98069e4f5c4fa5cf" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/vmanot/Merge.git", "state" : { "branch" : "master", - "revision" : "e8bc37c8dc203cab481efedd71237c151882c007" + "revision" : "2a47b62831164bea212a1616cfe3d4da32902f7f" } }, { @@ -34,7 +34,7 @@ "location" : "https://github.com/vmanot/Swallow.git", "state" : { "branch" : "master", - "revision" : "6227a1114e341daf54e90df61e173599b187a9b1" + "revision" : "f91811c49863ed506da4f3aed21f6c216a258ca0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -70,7 +70,7 @@ "location" : "https://github.com/SwiftUIX/SwiftUIX.git", "state" : { "branch" : "master", - "revision" : "836fc284a9bb07fc9ab6d2dce6ebd0e32aabde26" + "revision" : "275be4fd06b570b71fe1eb47b4246f4c4285b177" } } ], diff --git a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift index 386b9164..bd5e0234 100644 --- a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift +++ b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift @@ -644,6 +644,50 @@ extension OpenAI.APISpecification.RequestBodies { } } +extension OpenAI.APISpecification.RequestBodies { + struct CreateImageEdit: Codable { + enum CodingKeys: String, CodingKey { + case image + case prompt + case mask + case model + case numberOfImages = "n" + case size + case responseFormat = "response_format" + case user + } + + let image: Data + let prompt: String + let mask: Data? + let model: OpenAI.Model.DALL_E + let numberOfImages: Int + let size: OpenAI.Image.Size + let responseFormat: OpenAI.Client.ImageResponseFormat + let user: String? + + init( + image: Data, + prompt: String, + mask: Data? = nil, + model: OpenAI.Model.DALL_E = .dalle2, + numberOfImages: Int = 1, + size: OpenAI.Image.Size = .w1024h1024, + responseFormat: OpenAI.Client.ImageResponseFormat = .ephemeralURL, + user: String? = nil + ) { + self.image = image + self.prompt = prompt + self.mask = mask + self.model = model + self.numberOfImages = numberOfImages + self.size = size + self.responseFormat = responseFormat + self.user = user + } + } +} + extension OpenAI.APISpecification.RequestBodies { struct CreateVectorStore: Codable { enum CodingKeys: String, CodingKey { diff --git a/Sources/OpenAI/Intramodular/Models/OpenAI.Model.swift b/Sources/OpenAI/Intramodular/Models/OpenAI.Model.swift index 23505a92..e50eeff4 100644 --- a/Sources/OpenAI/Intramodular/Models/OpenAI.Model.swift +++ b/Sources/OpenAI/Intramodular/Models/OpenAI.Model.swift @@ -405,6 +405,7 @@ extension OpenAI.Model { } case dalle3 = "dall-e-3" + case dalle2 = "dall-e-2" public var contextSize: Int? { 4000 @@ -414,6 +415,8 @@ extension OpenAI.Model { switch self { case .dalle3: "dall-e-3" + case .dalle2: + "dall-e-2" } } } diff --git a/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift b/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift index 23124b55..c78d1528 100644 --- a/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift +++ b/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift @@ -3,6 +3,7 @@ // import CorePersistence +import SwiftUIX import Swallow extension OpenAI.Client { @@ -40,4 +41,11 @@ extension OpenAI.Client { return response } + + public func createImageEdit( + image: _AnyImage, + prompt: String + ) { + + } } From c53ff3dd58208eb5c6d493f88d6d18821caef7fb Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 3 Dec 2024 12:10:43 -0700 Subject: [PATCH 46/73] image edits --- ...penAI.APISpecification.RequestBodies.swift | 74 ++++++++++++++++++- .../API/OpenAI.APISpecification.swift | 8 ++ .../Intramodular/OpenAI.Client-Image.swift | 13 +++- 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift index bd5e0234..a2f6090d 100644 --- a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift +++ b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.RequestBodies.swift @@ -645,7 +645,7 @@ extension OpenAI.APISpecification.RequestBodies { } extension OpenAI.APISpecification.RequestBodies { - struct CreateImageEdit: Codable { + struct CreateImageEdit: Codable, HTTPRequest.Multipart.ContentConvertible { enum CodingKeys: String, CodingKey { case image case prompt @@ -685,6 +685,76 @@ extension OpenAI.APISpecification.RequestBodies { self.responseFormat = responseFormat self.user = user } + + func __conversion() -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .file( + named: "image", + data: image, + filename: "image.png", + contentType: .custom("image/png") + ) + ) + + result.append( + .text( + named: "prompt", + value: prompt + ) + ) + + if let mask = mask { + result.append( + .file( + named: "mask", + data: mask, + filename: "mask.png", + contentType: .custom("image/png") + ) + ) + } + + result.append( + .text( + named: "model", + value: model.rawValue + ) + ) + + result.append( + .text( + named: "n", + value: String(numberOfImages) + ) + ) + + result.append( + .text( + named: "size", + value: size.rawValue + ) + ) + + result.append( + .text( + named: "response_format", + value: responseFormat.rawValue + ) + ) + + if let user = user { + result.append( + .text( + named: "user", + value: user + ) + ) + } + + return result + } } } @@ -861,7 +931,7 @@ extension OpenAI.APISpecification.RequestBodies { /// The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases. If a seed is not specified, one will be generated for you. let seed: Int? } - + } extension OpenAI.APISpecification.RequestBodies { diff --git a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.swift b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.swift index 022f7506..edb65fad 100644 --- a/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.swift +++ b/Sources/OpenAI/Intramodular/API/OpenAI.APISpecification.swift @@ -203,6 +203,11 @@ extension OpenAI { @Body(json: .input, keyEncodingStrategy: .convertToSnakeCase) var createImage = Endpoint, Void>() + @POST + @Path("/v1/images/edits") + @Body(multipart: .input) + var createImageEdit = Endpoint, Void>() + // Vector Store @Header(["OpenAI-Beta": "assistants=v2"]) @POST @@ -354,6 +359,9 @@ extension OpenAI.APISpecification { context: DecodeOutputContext ) throws -> Output { do { + + print(response) + try response.validate() } catch { let apiError: Error diff --git a/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift b/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift index c78d1528..36956fd9 100644 --- a/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift +++ b/Sources/OpenAI/Intramodular/OpenAI.Client-Image.swift @@ -43,9 +43,18 @@ extension OpenAI.Client { } public func createImageEdit( - image: _AnyImage, + image: Data, prompt: String - ) { + ) async throws -> OpenAI.List { + let requestBody = OpenAI.APISpecification.RequestBodies.CreateImageEdit( + image: image, + prompt: prompt + ) + + let response = try await run(\.createImageEdit, with: requestBody) + print(response) + + return response } } From a040321e14f0a62e60df2cea19a9980c545c109f Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Wed, 11 Dec 2024 00:27:32 +0530 Subject: [PATCH 47/73] Add xAI --- Package.swift | 35 ++++ .../ModelIdentifier.Provider.swift | 5 + ...penAI.APISpecification.RequestBodies.swift | 2 +- .../Perplexity+LLMRequestHandling.swift | 1 - ...exity.APISpecification.RequestBodies.swift | 46 +++++- ...xity.APISpecification.ResponseBodies.swift | 1 + .../Intramodular/XAI+LLMRequestHandling.swift | 119 ++++++++++++++ .../XAI.APISpecification.RequestBodies.swift | 123 ++++++++++++++ .../XAI.APISpecification.ResponseBodies.swift | 10 ++ .../XAI.APISpecification.swift | 151 ++++++++++++++++++ .../xAI/Intramodular/XAI.ChatMessage.swift | 10 ++ Sources/xAI/Intramodular/XAI.Client.swift | 52 ++++++ Sources/xAI/Intramodular/XAI.Model.swift | 54 +++++++ Sources/xAI/Intramodular/XAI.swift | 9 ++ Sources/xAI/module.swift | 6 + .../Intramodular/CompletionTests.swift | 3 +- Tests/XAI/Intramodular/CompletionTests.swift | 34 ++++ 17 files changed, 651 insertions(+), 10 deletions(-) create mode 100644 Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift create mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift create mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift create mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift create mode 100644 Sources/xAI/Intramodular/XAI.ChatMessage.swift create mode 100644 Sources/xAI/Intramodular/XAI.Client.swift create mode 100644 Sources/xAI/Intramodular/XAI.Model.swift create mode 100644 Sources/xAI/Intramodular/XAI.swift create mode 100644 Sources/xAI/module.swift create mode 100644 Tests/XAI/Intramodular/CompletionTests.swift diff --git a/Package.swift b/Package.swift index ade916e7..2eba40c1 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,7 @@ let package = Package( "Ollama", "OpenAI", "Perplexity", + "XAI", "TogetherAI", "VoyageAI", "AI", @@ -50,6 +51,12 @@ let package = Package( targets: [ "Perplexity" ] + ), + .library( + name: "XAI", + targets: [ + "XAI" + ] ) ], dependencies: [ @@ -235,6 +242,22 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .target( + name: "XAI", + dependencies: [ + "CorePersistence", + "CoreMI", + "LargeLanguageModels", + "OpenAI", + "Merge", + "NetworkKit", + "Swallow" + ], + path: "Sources/XAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .target( name: "Jina", dependencies: [ @@ -353,6 +376,7 @@ let package = Package( "Ollama", "OpenAI", "Perplexity", + "XAI", "PlayHT", "Rime", "Swallow", @@ -419,6 +443,17 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .testTarget( + name: "XAITests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/XAI", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .testTarget( name: "PerplexityTests", dependencies: [ diff --git a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift index 1d3b94ce..516b52cb 100644 --- a/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift +++ b/Sources/CoreMI/Intramodular/Model Identifier/ModelIdentifier.Provider.swift @@ -28,6 +28,7 @@ extension ModelIdentifier { case _Rime case _HumeAI case _NeetsAI + case _xAI case unknown(String) @@ -138,6 +139,8 @@ extension ModelIdentifier.Provider: CustomStringConvertible { return "HumeAI" case ._NeetsAI: return "NeetsAI" + case ._xAI: + return "xAI" case .unknown(let provider): return provider } @@ -183,6 +186,8 @@ extension ModelIdentifier.Provider: RawRepresentable { return "humeAI" case ._NeetsAI: return "neetsAI" + case ._xAI: + return "xAI" case .unknown(let provider): return provider } diff --git a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.RequestBodies.swift b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.RequestBodies.swift index f0e988bf..fbfa56ee 100644 --- a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.RequestBodies.swift +++ b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.RequestBodies.swift @@ -12,7 +12,7 @@ extension OpenAI.APISpecification { } extension OpenAI.APISpecification.RequestBodies { - struct CreateCompletion: Codable, Hashable { + public struct CreateCompletion: Codable, Hashable { let prompt: Either let model: OpenAI.Model let suffix: String? diff --git a/Sources/Perplexity/Intramodular/Perplexity+LLMRequestHandling.swift b/Sources/Perplexity/Intramodular/Perplexity+LLMRequestHandling.swift index 7ecf63ba..2aa90bfd 100644 --- a/Sources/Perplexity/Intramodular/Perplexity+LLMRequestHandling.swift +++ b/Sources/Perplexity/Intramodular/Perplexity+LLMRequestHandling.swift @@ -70,7 +70,6 @@ extension Perplexity.Client: LLMRequestHandling { topP: parameters.temperatureOrTopP?.topProbabilityMass, topK: nil, maxTokens: parameters.tokenLimit?.fixedValue, - returnCitations: nil, returnImages: nil, stream: false, presencePenalty: nil, diff --git a/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.RequestBodies.swift b/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.RequestBodies.swift index e8104870..3581aabd 100644 --- a/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.RequestBodies.swift +++ b/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.RequestBodies.swift @@ -1,7 +1,3 @@ -// -// Copyright (c) Vatsal Manot -// - import Foundation extension Perplexity.APISpecification.RequestBodies { @@ -25,12 +21,18 @@ extension Perplexity.APISpecification.RequestBodies { /// The maximum number of completion tokens returned by the API. The total number of tokens requested in max_tokens plus the number of prompt tokens sent in messages must not exceed the context window token limit of model requested. If left unspecified, then the model will generate tokens until either it reaches its stop token or the end of its context window. public var maxTokens: Int? - /// Determines whether or not a request to an online model should return citations. Citations are in closed beta access. To gain access, apply at https://perplexity.typeform.com/to/j50rnNiB - public var returnCitations: Bool? + /// Given a list of domains, limit the citations used by the online model to URLs from the specified domains. Currently limited to only 3 domains for whitelisting and blacklisting. + public var searchDomainFilter: [String]? - /// Determines whether or not a request to an online model should return images. Images are in closed beta access. To gain access, apply at https://perplexity.typeform.com/to/j50rnNiB + /// Determines whether or not a request to an online model should return images. Images are in closed beta. public var returnImages: Bool? + /// Determines whether or not a request to an online model should return related questions. Related questions are in closed beta. + public var returnRelatedQuestions: Bool? + + /// Returns search results within the specified time interval - does not apply to images. Values include `month`, `week`, `day`, `hour`. + public var searchRecencyFilter: String? + /// Determines whether or not to incrementally stream the response with server-sent events with content-type: text/event-stream. public var stream: Bool? @@ -39,5 +41,35 @@ extension Perplexity.APISpecification.RequestBodies { /// A multiplicative penalty greater than 0. Values greater than 1.0 penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. A value of 1.0 means no penalty. Incompatible with presence_penalty. public var frequencyPenalty: Double? + + public init( + model: Perplexity.Model, + messages: [Perplexity.ChatMessage], + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + maxTokens: Int? = nil, + searchDomainFilter: [String]? = nil, + returnImages: Bool? = nil, + returnRelatedQuestions: Bool? = nil, + searchRecencyFilter: String? = nil, + stream: Bool? = nil, + presencePenalty: Double? = nil, + frequencyPenalty: Double? = nil + ) { + self.model = model + self.messages = messages + self.temperature = temperature + self.topP = topP + self.topK = topK + self.maxTokens = maxTokens + self.searchDomainFilter = searchDomainFilter + self.returnImages = returnImages + self.returnRelatedQuestions = returnRelatedQuestions + self.searchRecencyFilter = searchRecencyFilter + self.stream = stream + self.presencePenalty = presencePenalty + self.frequencyPenalty = frequencyPenalty + } } } diff --git a/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.ResponseBodies.swift b/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.ResponseBodies.swift index c96e4581..566d3145 100644 --- a/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.ResponseBodies.swift +++ b/Sources/Perplexity/Intramodular/Perplexity.APISpecification/Perplexity.APISpecification.ResponseBodies.swift @@ -36,6 +36,7 @@ extension Perplexity.APISpecification.ResponseBodies { public var object: String public var created: Date public var model: Perplexity.Model + public var citations: [String] public var choices: [Choice] public let usage: Usage } diff --git a/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift b/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift new file mode 100644 index 00000000..ba297618 --- /dev/null +++ b/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift @@ -0,0 +1,119 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import LargeLanguageModels +import NetworkKit +import OpenAI +import Swallow + +extension XAI.Client: _TaskDependenciesExporting { + public var _exportedTaskDependencies: TaskDependencies { + var result = TaskDependencies() + + result[\.llm] = self + + return result + } +} + +extension XAI.Client: LLMRequestHandling { + public var _availableModels: [ModelIdentifier]? { + XAI.Model.allCases.map({ $0.__conversion() }) + } + + public func complete( + prompt: Prompt, + parameters: Prompt.CompletionParameters + ) async throws -> Prompt.Completion { + let _completion: Any + + switch prompt { + case let prompt as AbstractLLM.TextPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + + case let prompt as AbstractLLM.ChatPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + default: + throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) + } + + return try cast(_completion) + } + + private func _complete( + prompt: AbstractLLM.TextPrompt, + parameters: AbstractLLM.TextCompletionParameters + ) async throws -> AbstractLLM.TextCompletion { + throw LLMRequestHandlingError.unsupportedPromptType(.init(Swift.type(of: prompt))) + } + + private func _complete( + prompt: AbstractLLM.ChatPrompt, + parameters: AbstractLLM.ChatCompletionParameters + ) async throws -> AbstractLLM.ChatCompletion { + let response: XAI.APISpecification.ResponseBodies.ChatCompletion = try await run( + \.chatCompletions, + with: .init( + messages: try await prompt.messages.asyncMap { + try await XAI.ChatMessage(from: $0) + }, + model: _model(for: prompt, parameters: parameters), + maxTokens: parameters.tokenLimit?.fixedValue, + temperature: parameters.temperatureOrTopP?.temperature + ) + ) + + assert(response.choices.count == 1) + + let message = try AbstractLLM.ChatMessage(from: response.choices[0]) + + return AbstractLLM.ChatCompletion( + prompt: prompt.messages, + message: message, + stopReason: .init() // FIXME: !!! + ) + } + + private func _model( + for prompt: AbstractLLM.ChatPrompt, + parameters: AbstractLLM.ChatCompletionParameters? + ) throws -> XAI.Model { + try prompt.context.get(\.modelIdentifier)?.as(XAI.Model.self) ?? XAI.Model.grokBeta + } +} + +// MARK: - Auxiliary + +extension AbstractLLM.ChatMessage { + public init( + from completion: XAI.APISpecification.ResponseBodies.ChatCompletion.Choice + ) throws { + try self.init(from: completion.message) + } +} + +extension XAI.ChatMessage.Role { + public init( + from role: AbstractLLM.ChatRole + ) throws { + switch role { + case .system: + self = .system + case .user: + self = .user + case .assistant: + self = .assistant + case .other: + throw Never.Reason.unsupported + } + } +} + diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift new file mode 100644 index 00000000..d98a79f0 --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift @@ -0,0 +1,123 @@ +import Foundation +import OpenAI + +extension XAI.APISpecification.RequestBodies { + public struct CreateChatCompletion: Codable, Hashable { + private enum CodingKeys: String, CodingKey { + case user + case messages + case functions = "functions" + case functionCallingStrategy = "function_call" + case model + case temperature + case topProbabilityMass = "top_p" + case choices = "n" + case stream + case stop + case maxTokens = "max_tokens" + case presencePenalty = "presence_penalty" + case frequencyPenalty = "frequency_penalty" + case logprobs = "logprobs" + case topLogprobs = "top_logprobs" + case logitBias = "logit_bias" + case responseFormat = "response_format" + case seed = "seed" + } + + let messages: [OpenAI.ChatMessage] + let model: XAI.Model + let frequencyPenalty: Double? + let logitBias: [String: Int]? + let logprobs: Bool? + let topLogprobs: Int? + let maxTokens: Int? + let choices: Int? + let presencePenalty: Double? + let responseFormat: OpenAI.ChatCompletion.ResponseFormat? + let seed: String? + let stop: [String]? + let stream: Bool? + let temperature: Double? + let topProbabilityMass: Double? + let user: String? + let functions: [OpenAI.ChatFunctionDefinition]? + let functionCallingStrategy: OpenAI.FunctionCallingStrategy? + + public init( + messages: [OpenAI.ChatMessage], + model: XAI.Model, + frequencyPenalty: Double? = nil, + logitBias: [String : Int]? = nil, + logprobs: Bool? = nil, + topLogprobs: Int? = nil, + maxTokens: Int? = nil, + choices: Int? = nil, + presencePenalty: Double? = nil, + responseFormat: OpenAI.ChatCompletion.ResponseFormat? = nil, + seed: String? = nil, + stop: [String]? = nil, + stream: Bool? = nil, + temperature: Double? = nil, + topProbabilityMass: Double? = nil, + user: String? = nil, + functions: [OpenAI.ChatFunctionDefinition]? = nil, + functionCallingStrategy: OpenAI.FunctionCallingStrategy? = nil + ) { + self.messages = messages + self.model = model + self.frequencyPenalty = frequencyPenalty + self.logitBias = logitBias + self.logprobs = logprobs + self.topLogprobs = topLogprobs + self.maxTokens = maxTokens + self.choices = choices + self.presencePenalty = presencePenalty + self.responseFormat = responseFormat + self.seed = seed + self.stop = stop + self.stream = stream + self.temperature = temperature + self.topProbabilityMass = topProbabilityMass + self.user = user + self.functions = functions + self.functionCallingStrategy = functionCallingStrategy + } + + public init( + user: String?, + messages: [OpenAI.ChatMessage], + functions: [OpenAI.ChatFunctionDefinition]?, + functionCallingStrategy: OpenAI.FunctionCallingStrategy?, + model: XAI.Model, + temperature: Double?, + topProbabilityMass: Double?, + choices: Int?, + stream: Bool?, + stop: [String]?, + maxTokens: Int?, + presencePenalty: Double?, + frequencyPenalty: Double?, + responseFormat: OpenAI.ChatCompletion.ResponseFormat? + ) { + self.user = user + self.messages = messages + self.functions = functions.nilIfEmpty() + self.functionCallingStrategy = functions == nil ? nil : functionCallingStrategy + self.model = model + self.temperature = temperature + self.topProbabilityMass = topProbabilityMass + self.choices = choices + self.stream = stream + self.stop = stop + self.maxTokens = maxTokens + self.presencePenalty = presencePenalty + self.frequencyPenalty = frequencyPenalty + self.logitBias = nil + self.logprobs = nil + self.topLogprobs = nil + self.responseFormat = responseFormat + self.seed = nil + } + } +} + diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift new file mode 100644 index 00000000..2e74dbe9 --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift @@ -0,0 +1,10 @@ +// +// Copyright (c) Vatsal Manot +// + +import Foundation +import OpenAI + +extension XAI.APISpecification.ResponseBodies { + public typealias ChatCompletion = OpenAI.ChatCompletion +} diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift new file mode 100644 index 00000000..054c5eda --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift @@ -0,0 +1,151 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import FoundationX +import OpenAI +import NetworkKit +import Swallow + +extension XAI { + public enum APIError: APIErrorProtocol { + public typealias API = XAI.APISpecification + + case apiKeyMissing + case incorrectAPIKeyProvided + case rateLimitExceeded + case badRequest(request: API.Request?, error: API.Request.Error) + case runtime(AnyError) + + public var traits: ErrorTraits { + [.domain(.networking)] + } + } + + public struct APISpecification: RESTAPISpecification { + public typealias Error = APIError + + public struct Configuration: Codable, Hashable { + public var apiKey: String? + } + + public let configuration: Configuration + + public var host: URL { + URL(string: "https://api.x.ai/")! + } + + public var id: some Hashable { + configuration + } + + @POST + @Path("v1/chat/completions") + public var chatCompletions = Endpoint() + } +} + +extension XAI.APISpecification { + public final class Endpoint: BaseHTTPEndpoint { + override public func buildRequestBase( + from input: Input, + context: BuildRequestContext + ) throws -> Request { + let configuration = context.root.configuration + + return try super + .buildRequestBase(from: input, context: context) + .jsonBody(input, keyEncodingStrategy: .convertToSnakeCase) + .header(.contentType(.json)) + .header(.accept(.json)) + .header(.authorization(.bearer, configuration.apiKey.unwrap())) + } + + struct _ErrorWrapper: Codable, Hashable, Sendable { + let detail: [ErrorDetail] + + struct ErrorDetail: Codable, Hashable, Sendable { + let loc: [ErrorLocation] + let msg: String + let type: String + + enum ErrorLocation: Codable, Hashable, Sendable { + case string(String) + case integer(Int) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let intValue = try? container.decode(Int.self) { + self = .integer(intValue) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode ErrorLocation") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .integer(let value): + try container.encode(value) + } + } + } + } + } + + override public func decodeOutputBase( + from response: Request.Response, + context: DecodeOutputContext + ) throws -> Output { + do { + try response.validate() + } catch { + let apiError: Error + + if let error = error as? Request.Error { + if let errorWrapper = try? response.decode( + _ErrorWrapper.self, + keyDecodingStrategy: .convertFromSnakeCase + ) { + let errorMsg = errorWrapper.detail.first?.msg ?? "" + if errorMsg.contains("You didn't provide an API key") { + throw Error.apiKeyMissing + } else if errorMsg.contains("Incorrect API key provided") { + throw Error.incorrectAPIKeyProvided + } + } + + if response.statusCode.rawValue == 429 { + apiError = .rateLimitExceeded + } else { + apiError = .badRequest(error) + } + } else { + apiError = .runtime(error) + } + + throw apiError + } + + return try response.decode( + Output.self, + keyDecodingStrategy: .convertFromSnakeCase + ) + } + } +} + +extension XAI.APISpecification { + public enum RequestBodies: _StaticSwift.Namespace { + + } + + public enum ResponseBodies: _StaticSwift.Namespace { + + } +} diff --git a/Sources/xAI/Intramodular/XAI.ChatMessage.swift b/Sources/xAI/Intramodular/XAI.ChatMessage.swift new file mode 100644 index 00000000..aab90fbc --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.ChatMessage.swift @@ -0,0 +1,10 @@ +// +// Copyright (c) Vatsal Manot +// + +import Foundation +import OpenAI + +extension XAI { + public typealias ChatMessage = OpenAI.ChatMessage +} diff --git a/Sources/xAI/Intramodular/XAI.Client.swift b/Sources/xAI/Intramodular/XAI.Client.swift new file mode 100644 index 00000000..b7dcb5bb --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.Client.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import LargeLanguageModels +import Merge +import NetworkKit +import Swallow + +extension XAI { + @RuntimeDiscoverable + public final class Client: HTTPClient, _StaticSwift.Namespace { + public static var persistentTypeRepresentation: some IdentityRepresentation { + CoreMI._ServiceVendorIdentifier._xAI + } + + public let interface: APISpecification + public let session: HTTPSession + + public init(interface: APISpecification, session: HTTPSession) { + self.interface = interface + self.session = session + } + + public convenience init(apiKey: String?) { + self.init( + interface: .init(configuration: .init(apiKey: apiKey)), + session: .shared + ) + } + } +} + +extension XAI.Client: CoreMI._ServiceClientProtocol { + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._xAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} diff --git a/Sources/xAI/Intramodular/XAI.Model.swift b/Sources/xAI/Intramodular/XAI.Model.swift new file mode 100644 index 00000000..152056b7 --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.Model.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) Vatsal Manot +// + +import CoreMI +import CorePersistence +import LargeLanguageModels +import Swallow + +extension XAI { + public enum Model: String, CaseIterable, Codable, Hashable, Named, Sendable { + case grokBeta = "grok-beta" + case grokVisionBeta = "grok-vision-beta" + + public var name: String { + switch self { + case .grokBeta: + return "Grok Beta" + case .grokVisionBeta: + return "Grok Vision Beta" + } + } + } +} + +// MARK: - Conformances + +extension XAI.Model: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension XAI.Model: ModelIdentifierRepresentable { + public init(from identifier: ModelIdentifier) throws { + guard identifier.provider == ._xAI, identifier.revision == nil else { + throw Never.Reason.illegal + } + + guard let model = Self(rawValue: identifier.name) else { + throw Never.Reason.unexpected + } + + self = model + } + + public func __conversion() -> ModelIdentifier { + ModelIdentifier( + provider: ._xAI, + name: rawValue, + revision: nil + ) + } +} diff --git a/Sources/xAI/Intramodular/XAI.swift b/Sources/xAI/Intramodular/XAI.swift new file mode 100644 index 00000000..66cdda47 --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.swift @@ -0,0 +1,9 @@ +// +// Copyright (c) Vatsal Manot +// + +import Swift + +public enum XAI { + +} diff --git a/Sources/xAI/module.swift b/Sources/xAI/module.swift new file mode 100644 index 00000000..877659d4 --- /dev/null +++ b/Sources/xAI/module.swift @@ -0,0 +1,6 @@ +// +// Copyright (c) Vatsal Manot +// + +@_exported import LargeLanguageModels +import Swift diff --git a/Tests/Perplexity/Intramodular/CompletionTests.swift b/Tests/Perplexity/Intramodular/CompletionTests.swift index 2f07169b..1decb677 100644 --- a/Tests/Perplexity/Intramodular/CompletionTests.swift +++ b/Tests/Perplexity/Intramodular/CompletionTests.swift @@ -55,8 +55,9 @@ final class CompletionTests: XCTestCase { AbstractLLM.ChatMessage( role: .user, body: "Sup?" - ) + ), ] + AbstractLLM.ChatCompletionStream let result: String = try await llm.complete( messages, diff --git a/Tests/XAI/Intramodular/CompletionTests.swift b/Tests/XAI/Intramodular/CompletionTests.swift new file mode 100644 index 00000000..96bb02cf --- /dev/null +++ b/Tests/XAI/Intramodular/CompletionTests.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) Vatsal Manot +// + +import LargeLanguageModels +import XAI +import XCTest + +final class CompletionTests: XCTestCase { + let llm: any LLMRequestHandling = client + + func testChatCompletions() async throws { + let llm: any LLMRequestHandling = client + + let messages: [AbstractLLM.ChatMessage] = [ + AbstractLLM.ChatMessage( + role: .system, + body: "You are an extremely intelligent assistant." + ), + AbstractLLM.ChatMessage( + role: .user, + body: "Sup?" + ) + ] + + let result: String = try await llm.complete( + messages, + model: XAI.Model.grokBeta, + as: .string + ) + + print(result) // "Hello! How can I assist you today?" + } +} From ecf11cb0bd581684cb36edf45e3f90d6d9e6da36 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Wed, 11 Dec 2024 01:23:57 +0530 Subject: [PATCH 48/73] Update package --- ...enAI.APISpecification.ResponseBodies.swift | 2 +- .../Intramodular/XAI+LLMRequestHandling.swift | 284 ++++++++++++++++-- .../XAI.APISpecification.ResponseBodies.swift | 2 +- .../XAI.APISpecification.swift | 8 +- .../xAI/Intramodular/XAI.ChatCompletion.swift | 238 +++++++++++++++ .../XAI.Client.ChatCompletionParameters.swift | 15 + Tests/XAI/module.swift | 14 + 7 files changed, 541 insertions(+), 22 deletions(-) create mode 100644 Sources/xAI/Intramodular/XAI.ChatCompletion.swift create mode 100644 Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift create mode 100644 Tests/XAI/module.swift diff --git a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.ResponseBodies.swift b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.ResponseBodies.swift index 640f4114..5ef07087 100644 --- a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.ResponseBodies.swift +++ b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.ResponseBodies.swift @@ -42,7 +42,7 @@ extension OpenAI.APISpecification.ResponseBodies { } } - struct CreateChatCompletion: Codable, Hashable, Sendable { + public struct CreateChatCompletion: Codable, Hashable, Sendable { public let message: OpenAI.ChatMessage } diff --git a/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift b/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift index ba297618..40a72feb 100644 --- a/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift +++ b/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift @@ -59,27 +59,37 @@ extension XAI.Client: LLMRequestHandling { prompt: AbstractLLM.ChatPrompt, parameters: AbstractLLM.ChatCompletionParameters ) async throws -> AbstractLLM.ChatCompletion { - let response: XAI.APISpecification.ResponseBodies.ChatCompletion = try await run( - \.chatCompletions, - with: .init( - messages: try await prompt.messages.asyncMap { - try await XAI.ChatMessage(from: $0) - }, - model: _model(for: prompt, parameters: parameters), - maxTokens: parameters.tokenLimit?.fixedValue, - temperature: parameters.temperatureOrTopP?.temperature - ) - ) + let model: XAI.Model = try self._model(for: prompt, parameters: parameters) + let parameters = try cast(parameters, to: AbstractLLM.ChatCompletionParameters.self) + let maxTokens: Int? - assert(response.choices.count == 1) + do { + switch (parameters.tokenLimit) { + case .fixed(let count): + maxTokens = count + case .max, .none: + maxTokens = nil + } + } - let message = try AbstractLLM.ChatMessage(from: response.choices[0]) + let completion: OpenAI.ChatCompletion = try await self.createChatCompletion( + messages: prompt.messages.asyncMap({ try await OpenAI.ChatMessage(from: $0) }), + model: model, + parameters: .init( + from: parameters, + model: model, + messages: prompt.messages, + maxTokens: maxTokens + ) + ) + + let message = try completion.choices.toCollectionOfOne().first.message return AbstractLLM.ChatCompletion( prompt: prompt.messages, - message: message, - stopReason: .init() // FIXME: !!! + message: try .init(from: message) ) + } private func _model( @@ -92,14 +102,43 @@ extension XAI.Client: LLMRequestHandling { // MARK: - Auxiliary -extension AbstractLLM.ChatMessage { +extension XAI.Client { + public func createChatCompletion( + messages: [XAI.ChatMessage], + model: XAI.Model, + parameters: XAI.Client.ChatCompletionParameters + ) async throws -> OpenAI.ChatCompletion { + let requestBody = XAI.APISpecification.RequestBodies.CreateChatCompletion( + messages: messages, + model: model, + parameters: parameters, + stream: false + ) + + return try await run(\.createChatCompletions, with: requestBody) + } +} + +extension XAI.Client.ChatCompletionParameters { public init( - from completion: XAI.APISpecification.ResponseBodies.ChatCompletion.Choice + from parameters: AbstractLLM.ChatCompletionParameters, + model: XAI.Model, + messages _: [AbstractLLM.ChatMessage], + maxTokens: Int? = nil ) throws { - try self.init(from: completion.message) + self.init( + temperature: parameters.temperatureOrTopP?.temperature, + topProbabilityMass: parameters.temperatureOrTopP?.topProbabilityMass, + stop: parameters.stops, + maxTokens: maxTokens, + functions: parameters.functions?.map { + OpenAI.ChatFunctionDefinition(from: $0) + } + ) } } + extension XAI.ChatMessage.Role { public init( from role: AbstractLLM.ChatRole @@ -117,3 +156,212 @@ extension XAI.ChatMessage.Role { } } +extension XAI.APISpecification.RequestBodies.CreateChatCompletion { + public init( + messages: [XAI.ChatMessage], + model: XAI.Model, + parameters: XAI.Client.ChatCompletionParameters, + user: String? = nil, + stream: Bool + ) { + self.init( + user: user, + messages: messages, + functions: parameters.functions, + functionCallingStrategy: parameters.functionCallingStrategy, + model: model, + temperature: parameters.temperature, + topProbabilityMass: parameters.topProbabilityMass, + choices: parameters.choices, + stream: stream, + stop: parameters.stop, + maxTokens: parameters.maxTokens, + presencePenalty: parameters.presencePenalty, + frequencyPenalty: parameters.frequencyPenalty, + responseFormat: parameters.responseFormat + ) + } +} + +/* + extension OpenAI.Client: LLMRequestHandling { + private var _debugPrintCompletions: Bool { + false + } + + public var _availableModels: [ModelIdentifier]? { + if let __cached_models { + do { + return try __cached_models.map({ try $0.__conversion() }) + } catch { + runtimeIssue(error) + } + } + + return OpenAI.Model.allCases.map({ $0.__conversion() }) + } + + public func complete( + prompt: Prompt, + parameters: Prompt.CompletionParameters + ) async throws -> Prompt.Completion { + let _completion: Any + + switch prompt { + case let prompt as AbstractLLM.TextPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + + case let prompt as AbstractLLM.ChatPrompt: + _completion = try await _complete( + prompt: prompt, + parameters: try cast(parameters) + ) + default: + throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) + } + + return try cast(_completion) + } + + private func _complete( + prompt: AbstractLLM.TextPrompt, + parameters: AbstractLLM.TextCompletionParameters + ) async throws -> AbstractLLM.TextCompletion { + let parameters = try cast(parameters, to: AbstractLLM.TextCompletionParameters.self) + + let model = OpenAI.Model.instructGPT(.davinci) + + let promptText = try prompt.prefix.promptLiteral + let completion = try await self.createCompletion( + model: model, + prompt: promptText._stripToText(), + parameters: .init( + from: parameters, + model: model, + prompt: prompt.prefix + ) + ) + + let text = try completion.choices.toCollectionOfOne().first.text + + _debugPrint( + prompt: prompt.debugDescription + .delimited(by: .quotationMark) + .delimited(by: "\n") + , + completion: text + .delimited(by: .quotationMark) + .delimited(by: "\n") + ) + + + return .init(prefix: promptText, text: text) + } + + private func _complete( + prompt: AbstractLLM.ChatPrompt, + parameters: AbstractLLM.ChatCompletionParameters + ) async throws -> AbstractLLM.ChatCompletion { + let model = try self._model(for: prompt, parameters: parameters) + let parameters = try cast(parameters, to: AbstractLLM.ChatCompletionParameters.self) + let maxTokens: Int? + + do { + switch (parameters.tokenLimit) { + case .fixed(let count): + maxTokens = count + case .max, .none: + /*let tokenizer = try await tokenizer(for: model) + let tokens = try tokenizer.encode(prompt._rawContent) + let contextSize = try model.contextSize + + maxTokens = contextSize - tokens.count*/ + maxTokens = nil + } + } + + let completion = try await self.createChatCompletion( + messages: prompt.messages.asyncMap({ try await OpenAI.ChatMessage(from: $0) }), + model: model, + parameters: .init( + from: parameters, + model: model, + messages: prompt.messages, + maxTokens: maxTokens + ) + ) + + let message = try completion.choices.toCollectionOfOne().first.message + + _debugPrint( + prompt: prompt.debugDescription, + completion: message.body + .description + .delimited(by: .quotationMark) + .delimited(by: "\n") + ) + + return AbstractLLM.ChatCompletion( + prompt: prompt.messages, + message: try .init(from: message) + ) + } + + private func _validateParameters( + parameters: AbstractLLM.ChatCompletionParameters, + model: OpenAI.Model + ) { + if let temperature = parameters.temperatureOrTopP?.temperature { + if temperature > 1.2 { + runtimeIssue("OpenAI's API doesn't seem to support a temperature higher than 1.2, but it is available on their playground at https://platform.openai.com/playground/chat?models=gpt-4o") + } + } + } + + public func completion( + for prompt: AbstractLLM.ChatPrompt + ) throws -> AbstractLLM.ChatCompletionStream { + AbstractLLM.ChatCompletionStream { + try await self._completion(for: prompt) + } + } + + private func _completion( + for prompt: AbstractLLM.ChatPrompt + ) async throws -> AnyPublisher { + var session: OpenAI.ChatCompletionSession! = OpenAI.ChatCompletionSession(client: self) + + let messages: [OpenAI.ChatMessage] = try await prompt.messages.asyncMap { + try await OpenAI.ChatMessage(from: $0) + } + let model: OpenAI.Model = try self._model(for: prompt, parameters: nil) + let parameters: OpenAI.Client.ChatCompletionParameters = try await self._chatCompletionParameters( + from: prompt.context.completionParameters, + for: prompt + ) + + return try await session + .complete( + messages: messages, + model: model, + parameters: parameters + ) + .tryMap { (message: OpenAI.ChatMessage) -> AbstractLLM.ChatCompletionStream.Event in + AbstractLLM.ChatCompletionStream.Event.completion( + AbstractLLM.ChatCompletion.Partial( + message: .init(whole: try AbstractLLM.ChatMessage(from: message)), + stopReason: nil + ) + ) + } + .handleCancelOrCompletion { _ in + session = nil + } + .eraseToAnyPublisher() + } + } + + */ diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift index 2e74dbe9..190cc950 100644 --- a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift +++ b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift @@ -6,5 +6,5 @@ import Foundation import OpenAI extension XAI.APISpecification.ResponseBodies { - public typealias ChatCompletion = OpenAI.ChatCompletion + } diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift index 054c5eda..0d3953d0 100644 --- a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift +++ b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift @@ -41,8 +41,12 @@ extension XAI { } @POST - @Path("v1/chat/completions") - public var chatCompletions = Endpoint() + @Path("/v1/chat/completions") + @Body(json: .input, keyEncodingStrategy: .convertToSnakeCase) + var createChatCompletions = Endpoint() + + + } } diff --git a/Sources/xAI/Intramodular/XAI.ChatCompletion.swift b/Sources/xAI/Intramodular/XAI.ChatCompletion.swift new file mode 100644 index 00000000..340deb4b --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.ChatCompletion.swift @@ -0,0 +1,238 @@ +// +// File.swift +// AI +// +// Created by Purav Manot on 11/12/24. +// + +import Foundation +import OpenAI + +extension XAI { + public final class ChatCompletion: XAI.Object { + private enum CodingKeys: String, CodingKey { + case id + case model + case createdAt = "created" + case choices + case usage + } + + public struct Choice: Codable, Hashable, Sendable { + public enum FinishReason: String, Codable, Hashable, Sendable { + case length = "length" + case stop = "stop" + case functionCall = "function_call" + } + + public let message: ChatMessage + public let index: Int + public let finishReason: FinishReason? + } + + public let id: String + public let model: XAI.Model + public let createdAt: Date + public let choices: [Choice] + public let usage: Usage + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(forKey: .id) + self.model = try container.decode(forKey: .model) + self.createdAt = try container.decode(forKey: .createdAt) + self.choices = try container.decode(forKey: .choices) + self.usage = try container.decode(forKey: .usage) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(model, forKey: .model) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(choices, forKey: .choices) + try container.encode(usage, forKey: .usage) + } + } + + public final class ChatCompletionChunk: XAI.Object { + private enum CodingKeys: String, CodingKey { + case id + case model + case createdAt = "created" + case choices + } + + public struct Choice: Codable, Hashable, Sendable { + public struct Delta: Codable, Hashable, Sendable { + public let role: OpenAI.ChatRole? + public let content: String? + } + + public enum FinishReason: String, Codable, Hashable, Sendable { + case length = "length" + case stop = "stop" + } + + public var delta: Delta + public let index: Int + public let finishReason: FinishReason? + } + + public let id: String + public let model: XAI.Model + public let createdAt: Date + public let choices: [Choice] + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(forKey: .id) + self.model = try container.decode(forKey: .model) + self.createdAt = try container.decode(forKey: .createdAt) + self.choices = try container.decode(forKey: .choices) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(model, forKey: .model) + try container.encode(createdAt, forKey: .createdAt) + try container.encode(choices, forKey: .choices) + } + } +} + +extension XAI { + public enum ObjectType: String, CaseIterable, Codable, TypeDiscriminator, Sendable { + case assistant = "assistant" + case assistantFile = "assistant.file" + case chatCompletion = "chat.completion" + case chatCompletionChunk = "chat.completion.chunk" + case embedding = "embedding" + case file = "file" + case image = "image" + case list + case message = "thread.message" + case model = "model" + case run = "thread.run" + case speech = "speech" + case thread = "thread" + case textCompletion = "text_completion" + case transcription = "transcription" + case vectorStore = "vector_store" + case vectorStoreDeleted = "vector_store.deleted" + + public static var _undiscriminatedType: Any.Type? { + OpenAI.Object.self + } + + public func resolveType() -> Any.Type { + switch self { + case .assistant: + return OpenAI.Assistant.self + case .assistantFile: + return OpenAI.AssistantFile.self + case .embedding: + return OpenAI.Embedding.self + case .file: + return OpenAI.File.self + case .chatCompletion: + return XAI.ChatCompletion.self + case .chatCompletionChunk: + return OpenAI.ChatCompletionChunk.self + case .image: + return OpenAI.Image.self + case .list: + return OpenAI.List.self + case .message: + return OpenAI.Message.self + case .model: + return OpenAI.ModelObject.self + case .run: + return OpenAI.Run.self + case .speech: + return OpenAI.Speech.self + case .textCompletion: + return OpenAI.TextCompletion.self + case .thread: + return OpenAI.Thread.self + case .transcription: + return OpenAI.AudioTranscription.self + case .vectorStore: + return OpenAI.VectorStore.self + case .vectorStoreDeleted: + return OpenAI.VectorStore.self + } + } + } +} + +extension XAI { + public class Object: Codable, PolymorphicDecodable, TypeDiscriminable { + private enum CodingKeys: String, CodingKey { + case type = "object" + } + + public let type: ObjectType + + public var typeDiscriminator: ObjectType { + type + } + + public init(type: ObjectType) { + self.type = type + } + + public required init( + from decoder: Decoder + ) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decodeIfPresent(ObjectType.self, forKey: .type) + + if let type { + self.type = type + } else if Self.self is OpenAI.AnyList.Type { + self.type = .list + } else { + self.type = try ObjectType.allCases.firstAndOnly(where: { $0.resolveType() == Self.self }).unwrap() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + } + } +} + +extension XAI { + public struct Usage: Codable, Hashable, Sendable { + public let promptTokens: Int + public let completionTokens: Int? + public let totalTokens: Int + public let promptTokensDetails: PromptTokensDetails? + public let completionTokensDetails: CompletionTokensDetails? + } + + public struct PromptTokensDetails: Codable, Hashable, Sendable { + public let cachedTokens: Int? + } + + public struct CompletionTokensDetails: Codable, Hashable, Sendable { + public let reasoningTokens: Int? + } +} diff --git a/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift b/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift new file mode 100644 index 00000000..75cea53b --- /dev/null +++ b/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift @@ -0,0 +1,15 @@ +// +// File.swift +// AI +// +// Created by Purav Manot on 11/12/24. +// + +import Foundation +import OpenAI + +extension XAI.Client { + public typealias ChatCompletionParameters = OpenAI.Client.ChatCompletionParameters +} + + diff --git a/Tests/XAI/module.swift b/Tests/XAI/module.swift new file mode 100644 index 00000000..313d61a5 --- /dev/null +++ b/Tests/XAI/module.swift @@ -0,0 +1,14 @@ +// +// Copyright (c) Vatsal Manot +// + +import XAI + +public var XAI_API_KEY: String { + "xai-EHvwOT7wQpdYyr49MtVkNmXnQPd9kJN1NhGdEpMcDCBN9Qawi3I0jii2a6lLUgIDMn1QHtzu8g88xOl8" +} + +public var client: XAI.Client { + XAI.Client(apiKey: XAI_API_KEY) +} + From e61c99d6eed57f9b41e62ed55bab3b9085e7578c Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 10 Dec 2024 14:37:14 -0700 Subject: [PATCH 49/73] removed print statements from debugging --- Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift | 2 -- .../OpenAI/Intramodular/API Client/OpenAI.Client-Image.swift | 2 -- .../API Specification/OpenAI.APISpecification.swift | 3 --- 3 files changed, 7 deletions(-) diff --git a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift index b78e11a9..cb71af84 100644 --- a/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift +++ b/Sources/HumeAI/Intramodular/API/HumeAI.APISpecification.swift @@ -496,8 +496,6 @@ extension HumeAI.APISpecification { from response: HTTPResponse, context: DecodeOutputContext ) throws -> Output { - - print(response) do { try response.validate() } catch { diff --git a/Sources/OpenAI/Intramodular/API Client/OpenAI.Client-Image.swift b/Sources/OpenAI/Intramodular/API Client/OpenAI.Client-Image.swift index 36956fd9..7a8b3ca1 100644 --- a/Sources/OpenAI/Intramodular/API Client/OpenAI.Client-Image.swift +++ b/Sources/OpenAI/Intramodular/API Client/OpenAI.Client-Image.swift @@ -53,8 +53,6 @@ extension OpenAI.Client { let response = try await run(\.createImageEdit, with: requestBody) - print(response) - return response } } diff --git a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.swift b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.swift index edb65fad..3e62f991 100644 --- a/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.swift +++ b/Sources/OpenAI/Intramodular/API Specification/OpenAI.APISpecification.swift @@ -359,9 +359,6 @@ extension OpenAI.APISpecification { context: DecodeOutputContext ) throws -> Output { do { - - print(response) - try response.validate() } catch { let apiError: Error From 939c487ba4da063a7e6fc611cf12de36c99d5d51 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Wed, 11 Dec 2024 15:20:32 +0530 Subject: [PATCH 50/73] Update package --- Package.swift | 61 ++- .../CoreMI._ServiceClientProtocol.swift | 2 +- Sources/Ollama/Intramodular/Ollama.swift | 2 +- .../Intramodular/XAI+LLMRequestHandling.swift | 367 ------------------ .../XAI.APISpecification.RequestBodies.swift | 123 ------ .../XAI.APISpecification.ResponseBodies.swift | 10 - .../XAI.APISpecification.swift | 155 -------- .../xAI/Intramodular/XAI.ChatCompletion.swift | 238 ------------ .../xAI/Intramodular/XAI.ChatMessage.swift | 10 - .../XAI.Client.ChatCompletionParameters.swift | 15 - Sources/xAI/Intramodular/XAI.Client.swift | 52 --- Sources/xAI/Intramodular/XAI.Model.swift | 54 --- Sources/xAI/Intramodular/XAI.swift | 9 - Sources/xAI/module.swift | 6 - Tests/XAI/Intramodular/CompletionTests.swift | 34 -- Tests/XAI/module.swift | 14 - 16 files changed, 28 insertions(+), 1124 deletions(-) delete mode 100644 Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift delete mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift delete mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift delete mode 100644 Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift delete mode 100644 Sources/xAI/Intramodular/XAI.ChatCompletion.swift delete mode 100644 Sources/xAI/Intramodular/XAI.ChatMessage.swift delete mode 100644 Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift delete mode 100644 Sources/xAI/Intramodular/XAI.Client.swift delete mode 100644 Sources/xAI/Intramodular/XAI.Model.swift delete mode 100644 Sources/xAI/Intramodular/XAI.swift delete mode 100644 Sources/xAI/module.swift delete mode 100644 Tests/XAI/Intramodular/CompletionTests.swift delete mode 100644 Tests/XAI/module.swift diff --git a/Package.swift b/Package.swift index 2eba40c1..eb9a8799 100644 --- a/Package.swift +++ b/Package.swift @@ -18,19 +18,14 @@ let package = Package( "CoreMI", "LargeLanguageModels", "Anthropic", - "Cohere", "ElevenLabs", "_Gemini", "Groq", "HuggingFace", - "Jina", "Mistral", "Ollama", "OpenAI", "Perplexity", - "XAI", - "TogetherAI", - "VoyageAI", "AI", ] ), @@ -40,6 +35,18 @@ let package = Package( "Anthropic" ] ), + .library( + name: "Cohere", + targets: [ + "Cohere" + ] + ), + .library( + name: "HumeAI", + targets: [ + "HumeAI" + ] + ), .library( name: "OpenAI", targets: [ @@ -53,9 +60,21 @@ let package = Package( ] ), .library( - name: "XAI", + name: "Jina", + targets: [ + "Jina" + ] + ), + .library( + name: "TogetherAI", + targets: [ + "TogetherAI" + ] + ), + .library( + name: "VoyageAI", targets: [ - "XAI" + "VoyageAI" ] ) ], @@ -242,22 +261,6 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), - .target( - name: "XAI", - dependencies: [ - "CorePersistence", - "CoreMI", - "LargeLanguageModels", - "OpenAI", - "Merge", - "NetworkKit", - "Swallow" - ], - path: "Sources/XAI", - swiftSettings: [ - .enableExperimentalFeature("AccessLevelOnImport") - ] - ), .target( name: "Jina", dependencies: [ @@ -376,7 +379,6 @@ let package = Package( "Ollama", "OpenAI", "Perplexity", - "XAI", "PlayHT", "Rime", "Swallow", @@ -443,17 +445,6 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), - .testTarget( - name: "XAITests", - dependencies: [ - "AI", - "Swallow" - ], - path: "Tests/XAI", - swiftSettings: [ - .enableExperimentalFeature("AccessLevelOnImport") - ] - ), .testTarget( name: "PerplexityTests", dependencies: [ diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift index 9fd23037..9b9efdb9 100644 --- a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift @@ -7,7 +7,7 @@ import Swallow extension CoreMI { /// A client for an AI/ML service. - public protocol _ServiceClientProtocol: PersistentlyRepresentableType { + public protocol _ServiceClientProtocol: AnyObject, PersistentlyRepresentableType { init(account: (any CoreMI._ServiceAccountProtocol)?) async throws } diff --git a/Sources/Ollama/Intramodular/Ollama.swift b/Sources/Ollama/Intramodular/Ollama.swift index 0bbb9583..491fe4e6 100644 --- a/Sources/Ollama/Intramodular/Ollama.swift +++ b/Sources/Ollama/Intramodular/Ollama.swift @@ -86,7 +86,7 @@ extension Ollama: CoreMI._ServiceClientProtocol { } -extension CoreMI._ServiceClientProtocol where Self == Ollama { +extension PersistentlyRepresentableType where Self == Ollama { public init( account: (any CoreMI._ServiceAccountProtocol)? ) async throws { diff --git a/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift b/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift deleted file mode 100644 index 40a72feb..00000000 --- a/Sources/xAI/Intramodular/XAI+LLMRequestHandling.swift +++ /dev/null @@ -1,367 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import LargeLanguageModels -import NetworkKit -import OpenAI -import Swallow - -extension XAI.Client: _TaskDependenciesExporting { - public var _exportedTaskDependencies: TaskDependencies { - var result = TaskDependencies() - - result[\.llm] = self - - return result - } -} - -extension XAI.Client: LLMRequestHandling { - public var _availableModels: [ModelIdentifier]? { - XAI.Model.allCases.map({ $0.__conversion() }) - } - - public func complete( - prompt: Prompt, - parameters: Prompt.CompletionParameters - ) async throws -> Prompt.Completion { - let _completion: Any - - switch prompt { - case let prompt as AbstractLLM.TextPrompt: - _completion = try await _complete( - prompt: prompt, - parameters: try cast(parameters) - ) - - case let prompt as AbstractLLM.ChatPrompt: - _completion = try await _complete( - prompt: prompt, - parameters: try cast(parameters) - ) - default: - throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) - } - - return try cast(_completion) - } - - private func _complete( - prompt: AbstractLLM.TextPrompt, - parameters: AbstractLLM.TextCompletionParameters - ) async throws -> AbstractLLM.TextCompletion { - throw LLMRequestHandlingError.unsupportedPromptType(.init(Swift.type(of: prompt))) - } - - private func _complete( - prompt: AbstractLLM.ChatPrompt, - parameters: AbstractLLM.ChatCompletionParameters - ) async throws -> AbstractLLM.ChatCompletion { - let model: XAI.Model = try self._model(for: prompt, parameters: parameters) - let parameters = try cast(parameters, to: AbstractLLM.ChatCompletionParameters.self) - let maxTokens: Int? - - do { - switch (parameters.tokenLimit) { - case .fixed(let count): - maxTokens = count - case .max, .none: - maxTokens = nil - } - } - - let completion: OpenAI.ChatCompletion = try await self.createChatCompletion( - messages: prompt.messages.asyncMap({ try await OpenAI.ChatMessage(from: $0) }), - model: model, - parameters: .init( - from: parameters, - model: model, - messages: prompt.messages, - maxTokens: maxTokens - ) - ) - - let message = try completion.choices.toCollectionOfOne().first.message - - return AbstractLLM.ChatCompletion( - prompt: prompt.messages, - message: try .init(from: message) - ) - - } - - private func _model( - for prompt: AbstractLLM.ChatPrompt, - parameters: AbstractLLM.ChatCompletionParameters? - ) throws -> XAI.Model { - try prompt.context.get(\.modelIdentifier)?.as(XAI.Model.self) ?? XAI.Model.grokBeta - } -} - -// MARK: - Auxiliary - -extension XAI.Client { - public func createChatCompletion( - messages: [XAI.ChatMessage], - model: XAI.Model, - parameters: XAI.Client.ChatCompletionParameters - ) async throws -> OpenAI.ChatCompletion { - let requestBody = XAI.APISpecification.RequestBodies.CreateChatCompletion( - messages: messages, - model: model, - parameters: parameters, - stream: false - ) - - return try await run(\.createChatCompletions, with: requestBody) - } -} - -extension XAI.Client.ChatCompletionParameters { - public init( - from parameters: AbstractLLM.ChatCompletionParameters, - model: XAI.Model, - messages _: [AbstractLLM.ChatMessage], - maxTokens: Int? = nil - ) throws { - self.init( - temperature: parameters.temperatureOrTopP?.temperature, - topProbabilityMass: parameters.temperatureOrTopP?.topProbabilityMass, - stop: parameters.stops, - maxTokens: maxTokens, - functions: parameters.functions?.map { - OpenAI.ChatFunctionDefinition(from: $0) - } - ) - } -} - - -extension XAI.ChatMessage.Role { - public init( - from role: AbstractLLM.ChatRole - ) throws { - switch role { - case .system: - self = .system - case .user: - self = .user - case .assistant: - self = .assistant - case .other: - throw Never.Reason.unsupported - } - } -} - -extension XAI.APISpecification.RequestBodies.CreateChatCompletion { - public init( - messages: [XAI.ChatMessage], - model: XAI.Model, - parameters: XAI.Client.ChatCompletionParameters, - user: String? = nil, - stream: Bool - ) { - self.init( - user: user, - messages: messages, - functions: parameters.functions, - functionCallingStrategy: parameters.functionCallingStrategy, - model: model, - temperature: parameters.temperature, - topProbabilityMass: parameters.topProbabilityMass, - choices: parameters.choices, - stream: stream, - stop: parameters.stop, - maxTokens: parameters.maxTokens, - presencePenalty: parameters.presencePenalty, - frequencyPenalty: parameters.frequencyPenalty, - responseFormat: parameters.responseFormat - ) - } -} - -/* - extension OpenAI.Client: LLMRequestHandling { - private var _debugPrintCompletions: Bool { - false - } - - public var _availableModels: [ModelIdentifier]? { - if let __cached_models { - do { - return try __cached_models.map({ try $0.__conversion() }) - } catch { - runtimeIssue(error) - } - } - - return OpenAI.Model.allCases.map({ $0.__conversion() }) - } - - public func complete( - prompt: Prompt, - parameters: Prompt.CompletionParameters - ) async throws -> Prompt.Completion { - let _completion: Any - - switch prompt { - case let prompt as AbstractLLM.TextPrompt: - _completion = try await _complete( - prompt: prompt, - parameters: try cast(parameters) - ) - - case let prompt as AbstractLLM.ChatPrompt: - _completion = try await _complete( - prompt: prompt, - parameters: try cast(parameters) - ) - default: - throw LLMRequestHandlingError.unsupportedPromptType(Prompt.self) - } - - return try cast(_completion) - } - - private func _complete( - prompt: AbstractLLM.TextPrompt, - parameters: AbstractLLM.TextCompletionParameters - ) async throws -> AbstractLLM.TextCompletion { - let parameters = try cast(parameters, to: AbstractLLM.TextCompletionParameters.self) - - let model = OpenAI.Model.instructGPT(.davinci) - - let promptText = try prompt.prefix.promptLiteral - let completion = try await self.createCompletion( - model: model, - prompt: promptText._stripToText(), - parameters: .init( - from: parameters, - model: model, - prompt: prompt.prefix - ) - ) - - let text = try completion.choices.toCollectionOfOne().first.text - - _debugPrint( - prompt: prompt.debugDescription - .delimited(by: .quotationMark) - .delimited(by: "\n") - , - completion: text - .delimited(by: .quotationMark) - .delimited(by: "\n") - ) - - - return .init(prefix: promptText, text: text) - } - - private func _complete( - prompt: AbstractLLM.ChatPrompt, - parameters: AbstractLLM.ChatCompletionParameters - ) async throws -> AbstractLLM.ChatCompletion { - let model = try self._model(for: prompt, parameters: parameters) - let parameters = try cast(parameters, to: AbstractLLM.ChatCompletionParameters.self) - let maxTokens: Int? - - do { - switch (parameters.tokenLimit) { - case .fixed(let count): - maxTokens = count - case .max, .none: - /*let tokenizer = try await tokenizer(for: model) - let tokens = try tokenizer.encode(prompt._rawContent) - let contextSize = try model.contextSize - - maxTokens = contextSize - tokens.count*/ - maxTokens = nil - } - } - - let completion = try await self.createChatCompletion( - messages: prompt.messages.asyncMap({ try await OpenAI.ChatMessage(from: $0) }), - model: model, - parameters: .init( - from: parameters, - model: model, - messages: prompt.messages, - maxTokens: maxTokens - ) - ) - - let message = try completion.choices.toCollectionOfOne().first.message - - _debugPrint( - prompt: prompt.debugDescription, - completion: message.body - .description - .delimited(by: .quotationMark) - .delimited(by: "\n") - ) - - return AbstractLLM.ChatCompletion( - prompt: prompt.messages, - message: try .init(from: message) - ) - } - - private func _validateParameters( - parameters: AbstractLLM.ChatCompletionParameters, - model: OpenAI.Model - ) { - if let temperature = parameters.temperatureOrTopP?.temperature { - if temperature > 1.2 { - runtimeIssue("OpenAI's API doesn't seem to support a temperature higher than 1.2, but it is available on their playground at https://platform.openai.com/playground/chat?models=gpt-4o") - } - } - } - - public func completion( - for prompt: AbstractLLM.ChatPrompt - ) throws -> AbstractLLM.ChatCompletionStream { - AbstractLLM.ChatCompletionStream { - try await self._completion(for: prompt) - } - } - - private func _completion( - for prompt: AbstractLLM.ChatPrompt - ) async throws -> AnyPublisher { - var session: OpenAI.ChatCompletionSession! = OpenAI.ChatCompletionSession(client: self) - - let messages: [OpenAI.ChatMessage] = try await prompt.messages.asyncMap { - try await OpenAI.ChatMessage(from: $0) - } - let model: OpenAI.Model = try self._model(for: prompt, parameters: nil) - let parameters: OpenAI.Client.ChatCompletionParameters = try await self._chatCompletionParameters( - from: prompt.context.completionParameters, - for: prompt - ) - - return try await session - .complete( - messages: messages, - model: model, - parameters: parameters - ) - .tryMap { (message: OpenAI.ChatMessage) -> AbstractLLM.ChatCompletionStream.Event in - AbstractLLM.ChatCompletionStream.Event.completion( - AbstractLLM.ChatCompletion.Partial( - message: .init(whole: try AbstractLLM.ChatMessage(from: message)), - stopReason: nil - ) - ) - } - .handleCancelOrCompletion { _ in - session = nil - } - .eraseToAnyPublisher() - } - } - - */ diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift deleted file mode 100644 index d98a79f0..00000000 --- a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.RequestBodies.swift +++ /dev/null @@ -1,123 +0,0 @@ -import Foundation -import OpenAI - -extension XAI.APISpecification.RequestBodies { - public struct CreateChatCompletion: Codable, Hashable { - private enum CodingKeys: String, CodingKey { - case user - case messages - case functions = "functions" - case functionCallingStrategy = "function_call" - case model - case temperature - case topProbabilityMass = "top_p" - case choices = "n" - case stream - case stop - case maxTokens = "max_tokens" - case presencePenalty = "presence_penalty" - case frequencyPenalty = "frequency_penalty" - case logprobs = "logprobs" - case topLogprobs = "top_logprobs" - case logitBias = "logit_bias" - case responseFormat = "response_format" - case seed = "seed" - } - - let messages: [OpenAI.ChatMessage] - let model: XAI.Model - let frequencyPenalty: Double? - let logitBias: [String: Int]? - let logprobs: Bool? - let topLogprobs: Int? - let maxTokens: Int? - let choices: Int? - let presencePenalty: Double? - let responseFormat: OpenAI.ChatCompletion.ResponseFormat? - let seed: String? - let stop: [String]? - let stream: Bool? - let temperature: Double? - let topProbabilityMass: Double? - let user: String? - let functions: [OpenAI.ChatFunctionDefinition]? - let functionCallingStrategy: OpenAI.FunctionCallingStrategy? - - public init( - messages: [OpenAI.ChatMessage], - model: XAI.Model, - frequencyPenalty: Double? = nil, - logitBias: [String : Int]? = nil, - logprobs: Bool? = nil, - topLogprobs: Int? = nil, - maxTokens: Int? = nil, - choices: Int? = nil, - presencePenalty: Double? = nil, - responseFormat: OpenAI.ChatCompletion.ResponseFormat? = nil, - seed: String? = nil, - stop: [String]? = nil, - stream: Bool? = nil, - temperature: Double? = nil, - topProbabilityMass: Double? = nil, - user: String? = nil, - functions: [OpenAI.ChatFunctionDefinition]? = nil, - functionCallingStrategy: OpenAI.FunctionCallingStrategy? = nil - ) { - self.messages = messages - self.model = model - self.frequencyPenalty = frequencyPenalty - self.logitBias = logitBias - self.logprobs = logprobs - self.topLogprobs = topLogprobs - self.maxTokens = maxTokens - self.choices = choices - self.presencePenalty = presencePenalty - self.responseFormat = responseFormat - self.seed = seed - self.stop = stop - self.stream = stream - self.temperature = temperature - self.topProbabilityMass = topProbabilityMass - self.user = user - self.functions = functions - self.functionCallingStrategy = functionCallingStrategy - } - - public init( - user: String?, - messages: [OpenAI.ChatMessage], - functions: [OpenAI.ChatFunctionDefinition]?, - functionCallingStrategy: OpenAI.FunctionCallingStrategy?, - model: XAI.Model, - temperature: Double?, - topProbabilityMass: Double?, - choices: Int?, - stream: Bool?, - stop: [String]?, - maxTokens: Int?, - presencePenalty: Double?, - frequencyPenalty: Double?, - responseFormat: OpenAI.ChatCompletion.ResponseFormat? - ) { - self.user = user - self.messages = messages - self.functions = functions.nilIfEmpty() - self.functionCallingStrategy = functions == nil ? nil : functionCallingStrategy - self.model = model - self.temperature = temperature - self.topProbabilityMass = topProbabilityMass - self.choices = choices - self.stream = stream - self.stop = stop - self.maxTokens = maxTokens - self.presencePenalty = presencePenalty - self.frequencyPenalty = frequencyPenalty - self.logitBias = nil - self.logprobs = nil - self.topLogprobs = nil - self.responseFormat = responseFormat - self.seed = nil - } - } -} - diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift deleted file mode 100644 index 190cc950..00000000 --- a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.ResponseBodies.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import Foundation -import OpenAI - -extension XAI.APISpecification.ResponseBodies { - -} diff --git a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift b/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift deleted file mode 100644 index 0d3953d0..00000000 --- a/Sources/xAI/Intramodular/XAI.APISpecification/XAI.APISpecification.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import FoundationX -import OpenAI -import NetworkKit -import Swallow - -extension XAI { - public enum APIError: APIErrorProtocol { - public typealias API = XAI.APISpecification - - case apiKeyMissing - case incorrectAPIKeyProvided - case rateLimitExceeded - case badRequest(request: API.Request?, error: API.Request.Error) - case runtime(AnyError) - - public var traits: ErrorTraits { - [.domain(.networking)] - } - } - - public struct APISpecification: RESTAPISpecification { - public typealias Error = APIError - - public struct Configuration: Codable, Hashable { - public var apiKey: String? - } - - public let configuration: Configuration - - public var host: URL { - URL(string: "https://api.x.ai/")! - } - - public var id: some Hashable { - configuration - } - - @POST - @Path("/v1/chat/completions") - @Body(json: .input, keyEncodingStrategy: .convertToSnakeCase) - var createChatCompletions = Endpoint() - - - - } -} - -extension XAI.APISpecification { - public final class Endpoint: BaseHTTPEndpoint { - override public func buildRequestBase( - from input: Input, - context: BuildRequestContext - ) throws -> Request { - let configuration = context.root.configuration - - return try super - .buildRequestBase(from: input, context: context) - .jsonBody(input, keyEncodingStrategy: .convertToSnakeCase) - .header(.contentType(.json)) - .header(.accept(.json)) - .header(.authorization(.bearer, configuration.apiKey.unwrap())) - } - - struct _ErrorWrapper: Codable, Hashable, Sendable { - let detail: [ErrorDetail] - - struct ErrorDetail: Codable, Hashable, Sendable { - let loc: [ErrorLocation] - let msg: String - let type: String - - enum ErrorLocation: Codable, Hashable, Sendable { - case string(String) - case integer(Int) - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let stringValue = try? container.decode(String.self) { - self = .string(stringValue) - } else if let intValue = try? container.decode(Int.self) { - self = .integer(intValue) - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode ErrorLocation") - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .string(let value): - try container.encode(value) - case .integer(let value): - try container.encode(value) - } - } - } - } - } - - override public func decodeOutputBase( - from response: Request.Response, - context: DecodeOutputContext - ) throws -> Output { - do { - try response.validate() - } catch { - let apiError: Error - - if let error = error as? Request.Error { - if let errorWrapper = try? response.decode( - _ErrorWrapper.self, - keyDecodingStrategy: .convertFromSnakeCase - ) { - let errorMsg = errorWrapper.detail.first?.msg ?? "" - if errorMsg.contains("You didn't provide an API key") { - throw Error.apiKeyMissing - } else if errorMsg.contains("Incorrect API key provided") { - throw Error.incorrectAPIKeyProvided - } - } - - if response.statusCode.rawValue == 429 { - apiError = .rateLimitExceeded - } else { - apiError = .badRequest(error) - } - } else { - apiError = .runtime(error) - } - - throw apiError - } - - return try response.decode( - Output.self, - keyDecodingStrategy: .convertFromSnakeCase - ) - } - } -} - -extension XAI.APISpecification { - public enum RequestBodies: _StaticSwift.Namespace { - - } - - public enum ResponseBodies: _StaticSwift.Namespace { - - } -} diff --git a/Sources/xAI/Intramodular/XAI.ChatCompletion.swift b/Sources/xAI/Intramodular/XAI.ChatCompletion.swift deleted file mode 100644 index 340deb4b..00000000 --- a/Sources/xAI/Intramodular/XAI.ChatCompletion.swift +++ /dev/null @@ -1,238 +0,0 @@ -// -// File.swift -// AI -// -// Created by Purav Manot on 11/12/24. -// - -import Foundation -import OpenAI - -extension XAI { - public final class ChatCompletion: XAI.Object { - private enum CodingKeys: String, CodingKey { - case id - case model - case createdAt = "created" - case choices - case usage - } - - public struct Choice: Codable, Hashable, Sendable { - public enum FinishReason: String, Codable, Hashable, Sendable { - case length = "length" - case stop = "stop" - case functionCall = "function_call" - } - - public let message: ChatMessage - public let index: Int - public let finishReason: FinishReason? - } - - public let id: String - public let model: XAI.Model - public let createdAt: Date - public let choices: [Choice] - public let usage: Usage - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.id = try container.decode(forKey: .id) - self.model = try container.decode(forKey: .model) - self.createdAt = try container.decode(forKey: .createdAt) - self.choices = try container.decode(forKey: .choices) - self.usage = try container.decode(forKey: .usage) - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(id, forKey: .id) - try container.encode(model, forKey: .model) - try container.encode(createdAt, forKey: .createdAt) - try container.encode(choices, forKey: .choices) - try container.encode(usage, forKey: .usage) - } - } - - public final class ChatCompletionChunk: XAI.Object { - private enum CodingKeys: String, CodingKey { - case id - case model - case createdAt = "created" - case choices - } - - public struct Choice: Codable, Hashable, Sendable { - public struct Delta: Codable, Hashable, Sendable { - public let role: OpenAI.ChatRole? - public let content: String? - } - - public enum FinishReason: String, Codable, Hashable, Sendable { - case length = "length" - case stop = "stop" - } - - public var delta: Delta - public let index: Int - public let finishReason: FinishReason? - } - - public let id: String - public let model: XAI.Model - public let createdAt: Date - public let choices: [Choice] - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.id = try container.decode(forKey: .id) - self.model = try container.decode(forKey: .model) - self.createdAt = try container.decode(forKey: .createdAt) - self.choices = try container.decode(forKey: .choices) - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(id, forKey: .id) - try container.encode(model, forKey: .model) - try container.encode(createdAt, forKey: .createdAt) - try container.encode(choices, forKey: .choices) - } - } -} - -extension XAI { - public enum ObjectType: String, CaseIterable, Codable, TypeDiscriminator, Sendable { - case assistant = "assistant" - case assistantFile = "assistant.file" - case chatCompletion = "chat.completion" - case chatCompletionChunk = "chat.completion.chunk" - case embedding = "embedding" - case file = "file" - case image = "image" - case list - case message = "thread.message" - case model = "model" - case run = "thread.run" - case speech = "speech" - case thread = "thread" - case textCompletion = "text_completion" - case transcription = "transcription" - case vectorStore = "vector_store" - case vectorStoreDeleted = "vector_store.deleted" - - public static var _undiscriminatedType: Any.Type? { - OpenAI.Object.self - } - - public func resolveType() -> Any.Type { - switch self { - case .assistant: - return OpenAI.Assistant.self - case .assistantFile: - return OpenAI.AssistantFile.self - case .embedding: - return OpenAI.Embedding.self - case .file: - return OpenAI.File.self - case .chatCompletion: - return XAI.ChatCompletion.self - case .chatCompletionChunk: - return OpenAI.ChatCompletionChunk.self - case .image: - return OpenAI.Image.self - case .list: - return OpenAI.List.self - case .message: - return OpenAI.Message.self - case .model: - return OpenAI.ModelObject.self - case .run: - return OpenAI.Run.self - case .speech: - return OpenAI.Speech.self - case .textCompletion: - return OpenAI.TextCompletion.self - case .thread: - return OpenAI.Thread.self - case .transcription: - return OpenAI.AudioTranscription.self - case .vectorStore: - return OpenAI.VectorStore.self - case .vectorStoreDeleted: - return OpenAI.VectorStore.self - } - } - } -} - -extension XAI { - public class Object: Codable, PolymorphicDecodable, TypeDiscriminable { - private enum CodingKeys: String, CodingKey { - case type = "object" - } - - public let type: ObjectType - - public var typeDiscriminator: ObjectType { - type - } - - public init(type: ObjectType) { - self.type = type - } - - public required init( - from decoder: Decoder - ) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let type = try container.decodeIfPresent(ObjectType.self, forKey: .type) - - if let type { - self.type = type - } else if Self.self is OpenAI.AnyList.Type { - self.type = .list - } else { - self.type = try ObjectType.allCases.firstAndOnly(where: { $0.resolveType() == Self.self }).unwrap() - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(type, forKey: .type) - } - } -} - -extension XAI { - public struct Usage: Codable, Hashable, Sendable { - public let promptTokens: Int - public let completionTokens: Int? - public let totalTokens: Int - public let promptTokensDetails: PromptTokensDetails? - public let completionTokensDetails: CompletionTokensDetails? - } - - public struct PromptTokensDetails: Codable, Hashable, Sendable { - public let cachedTokens: Int? - } - - public struct CompletionTokensDetails: Codable, Hashable, Sendable { - public let reasoningTokens: Int? - } -} diff --git a/Sources/xAI/Intramodular/XAI.ChatMessage.swift b/Sources/xAI/Intramodular/XAI.ChatMessage.swift deleted file mode 100644 index aab90fbc..00000000 --- a/Sources/xAI/Intramodular/XAI.ChatMessage.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import Foundation -import OpenAI - -extension XAI { - public typealias ChatMessage = OpenAI.ChatMessage -} diff --git a/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift b/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift deleted file mode 100644 index 75cea53b..00000000 --- a/Sources/xAI/Intramodular/XAI.Client.ChatCompletionParameters.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// File.swift -// AI -// -// Created by Purav Manot on 11/12/24. -// - -import Foundation -import OpenAI - -extension XAI.Client { - public typealias ChatCompletionParameters = OpenAI.Client.ChatCompletionParameters -} - - diff --git a/Sources/xAI/Intramodular/XAI.Client.swift b/Sources/xAI/Intramodular/XAI.Client.swift deleted file mode 100644 index b7dcb5bb..00000000 --- a/Sources/xAI/Intramodular/XAI.Client.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CorePersistence -import LargeLanguageModels -import Merge -import NetworkKit -import Swallow - -extension XAI { - @RuntimeDiscoverable - public final class Client: HTTPClient, _StaticSwift.Namespace { - public static var persistentTypeRepresentation: some IdentityRepresentation { - CoreMI._ServiceVendorIdentifier._xAI - } - - public let interface: APISpecification - public let session: HTTPSession - - public init(interface: APISpecification, session: HTTPSession) { - self.interface = interface - self.session = session - } - - public convenience init(apiKey: String?) { - self.init( - interface: .init(configuration: .init(apiKey: apiKey)), - session: .shared - ) - } - } -} - -extension XAI.Client: CoreMI._ServiceClientProtocol { - public convenience init( - account: (any CoreMI._ServiceAccountProtocol)? - ) async throws { - let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() - let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() - - guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._xAI else { - throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) - } - - guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { - throw CoreMI._ServiceClientError.invalidCredential(try account.credential) - } - - self.init(apiKey: credential.apiKey) - } -} diff --git a/Sources/xAI/Intramodular/XAI.Model.swift b/Sources/xAI/Intramodular/XAI.Model.swift deleted file mode 100644 index 152056b7..00000000 --- a/Sources/xAI/Intramodular/XAI.Model.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import CoreMI -import CorePersistence -import LargeLanguageModels -import Swallow - -extension XAI { - public enum Model: String, CaseIterable, Codable, Hashable, Named, Sendable { - case grokBeta = "grok-beta" - case grokVisionBeta = "grok-vision-beta" - - public var name: String { - switch self { - case .grokBeta: - return "Grok Beta" - case .grokVisionBeta: - return "Grok Vision Beta" - } - } - } -} - -// MARK: - Conformances - -extension XAI.Model: CustomStringConvertible { - public var description: String { - rawValue - } -} - -extension XAI.Model: ModelIdentifierRepresentable { - public init(from identifier: ModelIdentifier) throws { - guard identifier.provider == ._xAI, identifier.revision == nil else { - throw Never.Reason.illegal - } - - guard let model = Self(rawValue: identifier.name) else { - throw Never.Reason.unexpected - } - - self = model - } - - public func __conversion() -> ModelIdentifier { - ModelIdentifier( - provider: ._xAI, - name: rawValue, - revision: nil - ) - } -} diff --git a/Sources/xAI/Intramodular/XAI.swift b/Sources/xAI/Intramodular/XAI.swift deleted file mode 100644 index 66cdda47..00000000 --- a/Sources/xAI/Intramodular/XAI.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import Swift - -public enum XAI { - -} diff --git a/Sources/xAI/module.swift b/Sources/xAI/module.swift deleted file mode 100644 index 877659d4..00000000 --- a/Sources/xAI/module.swift +++ /dev/null @@ -1,6 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -@_exported import LargeLanguageModels -import Swift diff --git a/Tests/XAI/Intramodular/CompletionTests.swift b/Tests/XAI/Intramodular/CompletionTests.swift deleted file mode 100644 index 96bb02cf..00000000 --- a/Tests/XAI/Intramodular/CompletionTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import LargeLanguageModels -import XAI -import XCTest - -final class CompletionTests: XCTestCase { - let llm: any LLMRequestHandling = client - - func testChatCompletions() async throws { - let llm: any LLMRequestHandling = client - - let messages: [AbstractLLM.ChatMessage] = [ - AbstractLLM.ChatMessage( - role: .system, - body: "You are an extremely intelligent assistant." - ), - AbstractLLM.ChatMessage( - role: .user, - body: "Sup?" - ) - ] - - let result: String = try await llm.complete( - messages, - model: XAI.Model.grokBeta, - as: .string - ) - - print(result) // "Hello! How can I assist you today?" - } -} diff --git a/Tests/XAI/module.swift b/Tests/XAI/module.swift deleted file mode 100644 index 313d61a5..00000000 --- a/Tests/XAI/module.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright (c) Vatsal Manot -// - -import XAI - -public var XAI_API_KEY: String { - "xai-EHvwOT7wQpdYyr49MtVkNmXnQPd9kJN1NhGdEpMcDCBN9Qawi3I0jii2a6lLUgIDMn1QHtzu8g88xOl8" -} - -public var client: XAI.Client { - XAI.Client(apiKey: XAI_API_KEY) -} - From 3d684484a0fbec77cbbc77447ad66eaa7d70da66 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Thu, 12 Dec 2024 01:00:16 +0530 Subject: [PATCH 51/73] Update package --- Sources/_Gemini/Intramodular/_Gemini.Model.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/_Gemini/Intramodular/_Gemini.Model.swift b/Sources/_Gemini/Intramodular/_Gemini.Model.swift index 7169e784..9677aff9 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Model.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Model.swift @@ -7,14 +7,17 @@ import Swift extension _Gemini { public enum Model: String, CaseIterable, Codable, Hashable, Sendable { + case gemini_2_0_flash_exp = "gemini-2.0-flash-exp" case gemini_1_5_pro = "gemini-1.5-pro" case gemini_1_5_pro_latest = "gemini-1.5-pro-latest" case gemini_1_5_flash = "gemini-1.5-flash" case gemini_1_5_flash_latest = "gemini-1.5-flash-latest" case gemini_1_0_pro = "gemini-1.0-pro" - + public var maximumContextLength: Int { switch self { + case .gemini_2_0_flash_exp: + return 1048576 case .gemini_1_5_pro: return 1048576 case .gemini_1_5_pro_latest: From 9339454a137dfa856a7b280af54dda4935fecc50 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Thu, 12 Dec 2024 01:09:37 +0530 Subject: [PATCH 52/73] Update package --- Package.swift | 17 +++++++++++++++++ Tests/_Gemini/module.swift | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 Tests/_Gemini/module.swift diff --git a/Package.swift b/Package.swift index eb9a8799..e7ddb997 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,12 @@ let package = Package( "AI", ] ), + .library( + name: "_Gemini", + targets: [ + "_Gemini" + ] + ), .library( name: "Anthropic", targets: [ @@ -401,6 +407,17 @@ let package = Package( .enableExperimentalFeature("AccessLevelOnImport") ] ), + .testTarget( + name: "_GeminiTests", + dependencies: [ + "AI", + "Swallow" + ], + path: "Tests/_Gemini", + swiftSettings: [ + .enableExperimentalFeature("AccessLevelOnImport") + ] + ), .testTarget( name: "AnthropicTests", dependencies: [ diff --git a/Tests/_Gemini/module.swift b/Tests/_Gemini/module.swift new file mode 100644 index 00000000..230031af --- /dev/null +++ b/Tests/_Gemini/module.swift @@ -0,0 +1,9 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import _Gemini + +var GEMINI_API_KEY: String { + "API_KEY" +} From 676ef05101efad6d5f253406df5563b7870ecd33 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Thu, 2 Jan 2025 08:15:53 -0700 Subject: [PATCH 53/73] language support for playht --- Package.resolved | 191 +++++++++++++++++- ...layHT.APISpecification.RequestBodies.swift | 19 +- .../API/PlayHT.APISpecification.swift | 7 +- .../Intramodular/Models/PlayHT.Voice.swift | 21 ++ .../PlayHT/Intramodular/PlayHT.Client.swift | 32 +-- .../PlayHT/Intramodular/PlayHT.Model.swift | 4 + 6 files changed, 236 insertions(+), 38 deletions(-) diff --git a/Package.resolved b/Package.resolved index 44c6cdec..23ea1ed3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,33 @@ { - "originHash" : "094840915419b625ed8a43083bdf164ab8d3f6bbb7fda2dcec07cb5e55a2b736", + "originHash" : "a7afdc02c33e043aef77037e39006816570e8a6bd0c65d3499a410c01f230282", "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "chatkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PreternaturalAI/ChatKit.git", + "state" : { + "branch" : "main", + "revision" : "8b19deb1b0f74f091fec9a8c4755998a00f2b4cb" + } + }, { "identity" : "corepersistence", "kind" : "remoteSourceControl", @@ -10,6 +37,87 @@ "revision" : "3fc10b8e55c3be60ca4695200cecfc046c0ba29a" } }, + { + "identity" : "fal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/preternatural-fork/Fal", + "state" : { + "revision" : "a58ca8a926a56a69ba3c454583f626b3629a4223", + "version" : "0.5.6" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "2e02253fd1ce99145bcbf1bb367ccf61bd0ca46b", + "version" : "11.6.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "4f234bcbdae841d7015258fbbf8e7743a39b8200", + "version" : "11.4.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", + "version" : "1.65.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "5cfe5f090c982de9c58605d2a82a4fc77b774fbd", + "version" : "4.1.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, { "identity" : "merge", "kind" : "remoteSourceControl", @@ -19,6 +127,15 @@ "revision" : "4bc71ce650b79b3dbe1a26acf7e54b29d750e0b6" } }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, { "identity" : "networkkit", "kind" : "remoteSourceControl", @@ -28,6 +145,24 @@ "revision" : "8daa1ba22e5d18e1b8e469d5a0dd0c58b675eb87" } }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "sideproject", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PreternaturalAI/Sideproject", + "state" : { + "branch" : "main", + "revision" : "82a434ea0c586612c0facb18108948547e24b9ee" + } + }, { "identity" : "swallow", "kind" : "remoteSourceControl", @@ -46,6 +181,24 @@ "version" : "1.1.4" } }, + { + "identity" : "swift-msgpack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nnabeyang/swift-msgpack.git", + "state" : { + "revision" : "7843723ab63aae2d7fa3b30a86cd1da578a441a3", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -64,6 +217,15 @@ "revision" : "3e47cc5f9b0cefe9ed1d0971aff22583bd9ac7b0" } }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect", + "state" : { + "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", + "version" : "0.12.0" + } + }, { "identity" : "swiftuix", "kind" : "remoteSourceControl", @@ -72,6 +234,33 @@ "branch" : "master", "revision" : "50e2aacd7b124ffb5d06b4bfa5a4f255052a559b" } + }, + { + "identity" : "swiftuiz", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftUIX/SwiftUIZ.git", + "state" : { + "branch" : "main", + "revision" : "194190e802249ba05e02903d06471eaff024caa0" + } + }, + { + "identity" : "swipeactions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aheze/SwipeActions", + "state" : { + "revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab", + "version" : "1.1.0" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } } ], "version" : 3 diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift index 79ddf26e..563b91c1 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.RequestBodies.swift @@ -15,7 +15,7 @@ extension PlayHT.APISpecification { public let text: String public let voice: String public let voiceEngine: PlayHT.Model - public let quality: String +// public let quality: String public let outputFormat: String // public let speed: Double? @@ -26,10 +26,11 @@ extension PlayHT.APISpecification { // public let voiceGuidance: Double? // public let styleGuidance: Double? // public let textGuidance: Double? - // public let language: String? + public let language: String? // private enum CodingKeys: String, CodingKey { - case text, voice, quality + case text, voice +// case quality case voiceEngine = "voice_engine" case outputFormat = "output_format" // case speed @@ -38,15 +39,15 @@ extension PlayHT.APISpecification { // case voiceGuidance = "voice_guidance" // case styleGuidance = "style_guidance" // case textGuidance = "text_guidance" - // case language + case language } public init( text: String, voice: String, voiceEngine: PlayHT.Model = .playHT2, - quality: String = "medium", - outputFormat: String = "mp3" +// quality: String = "medium", + outputFormat: String = "mp3", // speed: Double? = nil, // sampleRate: Int? = 48000, // seed: Int? = nil, @@ -55,12 +56,12 @@ extension PlayHT.APISpecification { // voiceGuidance: Double? = nil, // styleGuidance: Double? = nil, // textGuidance: Double? = nil, - // language: String? = nil + language: String? = nil ) { self.text = text self.voice = voice self.voiceEngine = voiceEngine - self.quality = quality +// self.quality = quality self.outputFormat = outputFormat // self.speed = speed // self.sampleRate = sampleRate @@ -70,7 +71,7 @@ extension PlayHT.APISpecification { // self.voiceGuidance = voiceGuidance // self.styleGuidance = styleGuidance // self.textGuidance = textGuidance - // self.language = language + self.language = language } } diff --git a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift index 525ef06d..bbe1e997 100644 --- a/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift +++ b/Sources/PlayHT/Intramodular/API/PlayHT.APISpecification.swift @@ -71,7 +71,7 @@ extension PlayHT { @POST @Path("/tts/stream") @Body(json: \.input, keyEncodingStrategy: .convertToSnakeCase) - var streamTextToSpeech = Endpoint() + var streamTextToSpeech = Endpoint() @GET @Path("/cloned-voices") @@ -107,10 +107,10 @@ extension PlayHT.APISpecification { from: input, context: context ) - + request = request .header("X-USER-ID", context.root.configuration.userId) - .header("accept", "application/json") + .header(.accept(.mpeg)) .header("AUTHORIZATION", context.root.configuration.apiKey) .header(.contentType(.json)) @@ -122,6 +122,7 @@ extension PlayHT.APISpecification { context: DecodeOutputContext ) throws -> Output { do { + dump(response) try response.validate() } catch { let apiError: Error diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift index 3ac8907f..c9708c13 100644 --- a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift @@ -31,6 +31,27 @@ extension PlayHT { case id, name, language, languageCode, voiceEngine, isCloned case gender, accent, age, style, sample, texture, loudness, tempo } + + public init( + id: String, + name: String, + language: String + ) { + self.id = .init(rawValue: id) + self.name = name + self.language = language + self.languageCode = nil + self.voiceEngine = "" + self.isCloned = nil + self.gender = nil + self.accent = nil + self.age = nil + self.style = nil + self.sample = nil + self.texture = nil + self.loudness = nil + self.tempo = nil + } // Add custom decoding if needed to handle any special cases public init(from decoder: Decoder) throws { diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 66e6e80f..e98849ee 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -63,7 +63,7 @@ extension PlayHT.Client { async let clonedVoices = clonedVoices() let (available, cloned) = try await (htVoices, clonedVoices) - return available + cloned + return cloned } public func availableVoices() async throws -> [PlayHT.Voice] { @@ -78,6 +78,7 @@ extension PlayHT.Client { text: String, voice: String, settings: PlayHT.VoiceSettings, + language: String, outputSettings: PlayHT.OutputSettings = .default, model: PlayHT.Model ) async throws -> Data { @@ -86,33 +87,14 @@ extension PlayHT.Client { text: text, voice: voice, voiceEngine: model, - quality: outputSettings.quality.rawValue, - outputFormat: outputSettings.format.rawValue +// quality: outputSettings.quality.rawValue, + outputFormat: outputSettings.format.rawValue, + language: language ) let responseData = try await run(\.streamTextToSpeech, with: input) - - guard let url = URL(string: responseData.href) else { - throw PlayHTError.invalidURL - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue(interface.configuration.userId ?? "", forHTTPHeaderField: "X-USER-ID") - request.addValue(interface.configuration.apiKey ?? "", forHTTPHeaderField: "AUTHORIZATION") - - let (audioData, response) = try await URLSession.shared.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { - throw PlayHTError.audioFetchFailed - } - - guard !audioData.isEmpty else { - throw PlayHTError.audioFetchFailed - } - - return audioData + + return responseData } diff --git a/Sources/PlayHT/Intramodular/PlayHT.Model.swift b/Sources/PlayHT/Intramodular/PlayHT.Model.swift index 9eeb0a28..20579363 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Model.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Model.swift @@ -18,6 +18,10 @@ extension PlayHT { case playHT1 = "PlayHT1.0" case playHT2Turbo = "PlayHT2.0-turbo" + + case play3_0Mini = "Play3.0-mini" + + case playDialog = "PlayDialog" } } From 520073a4811c58ce98baf1d8c038e454a0f6172c Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Wed, 8 Jan 2025 18:11:17 -0700 Subject: [PATCH 54/73] ElevenLabs+Dubbing --- ...nLabs.APISpecification.RequestBodies.swift | 185 ++++++++++++++++++ ...Labs.APISpecification.ResponseBodies.swift | 23 +++ .../API/ElevenLabs.APISpecification.swift | 18 ++ .../ElevenLabs.Client+Dubbing.swift | 83 ++++++++ .../Intramodular/ElevenLabs.Model.swift | 2 + .../Models/ElevenLabs.DubbingOptions.swift | 33 ++++ .../Models/ElevenLabs.DubbingProgress.swift | 16 ++ .../Models/ElevenLabs.DubbingResult.swift | 16 ++ .../{ => Models}/ElevenLabs.Voice.swift | 0 .../ElevenLabs.VoiceSettings.swift | 0 SwallowUI | 1 + 11 files changed, 377 insertions(+) create mode 100644 Sources/ElevenLabs/Intramodular/ElevenLabs.Client+Dubbing.swift create mode 100644 Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingOptions.swift create mode 100644 Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingProgress.swift create mode 100644 Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingResult.swift rename Sources/ElevenLabs/Intramodular/{ => Models}/ElevenLabs.Voice.swift (100%) rename Sources/ElevenLabs/Intramodular/{ => Models}/ElevenLabs.VoiceSettings.swift (100%) create mode 160000 SwallowUI diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift index 3404e48e..1b72b81b 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift @@ -14,6 +14,7 @@ extension ElevenLabs.APISpecification { enum RequestBodies { public struct SpeechRequest: Codable, Hashable, Equatable { public let text: String + public let languageCode: String? public let voiceSettings: ElevenLabs.VoiceSettings public let model: ElevenLabs.Model @@ -21,14 +22,17 @@ extension ElevenLabs.APISpecification { case text case voiceSettings = "voice_settings" case model = "model_id" + case languageCode = "language_code" } public init( text: String, + languageCode: String?, voiceSettings: ElevenLabs.VoiceSettings, model: ElevenLabs.Model ) { self.text = text + self.languageCode = languageCode self.voiceSettings = voiceSettings self.model = model } @@ -47,17 +51,20 @@ extension ElevenLabs.APISpecification { public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { public let voiceId: String public let audioURL: URL + public let languageCode: String? public let model: ElevenLabs.Model public let voiceSettings: ElevenLabs.VoiceSettings public init( voiceId: String, audioURL: URL, + languageCode: String?, model: ElevenLabs.Model, voiceSettings: ElevenLabs.VoiceSettings ) { self.voiceId = voiceId self.audioURL = audioURL + self.languageCode = languageCode self.model = model self.voiceSettings = voiceSettings } @@ -72,6 +79,15 @@ extension ElevenLabs.APISpecification { ) ) + if let languageCode { + result.append( + .text( + named: "language_code", + value: languageCode + ) + ) + } + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase if let voiceSettingsData = try? encoder.encode(voiceSettings), @@ -201,5 +217,174 @@ extension ElevenLabs.APISpecification { return result } } + + public struct DubbingRequest: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let name: String? + public let sourceURL: URL? + public let sourceLang: String? + public let targetLang: String + public let numSpeakers: Int? + public let watermark: Bool? + public let startTime: Int? + public let endTime: Int? + public let highestResolution: Bool? + public let dropBackgroundAudio: Bool? + public let useProfanityFilter: Bool? + public let fileData: Data? + + public init( + name: String? = nil, + sourceURL: URL? = nil, + sourceLang: String? = nil, + targetLang: String, + numSpeakers: Int? = nil, + watermark: Bool? = nil, + startTime: Int? = nil, + endTime: Int? = nil, + highestResolution: Bool? = nil, + dropBackgroundAudio: Bool? = nil, + useProfanityFilter: Bool? = nil, + fileData: Data? = nil + ) { + self.name = name + self.sourceURL = sourceURL + self.sourceLang = sourceLang + self.targetLang = targetLang + self.numSpeakers = numSpeakers + self.watermark = watermark + self.startTime = startTime + self.endTime = endTime + self.highestResolution = highestResolution + self.dropBackgroundAudio = dropBackgroundAudio + self.useProfanityFilter = useProfanityFilter + self.fileData = fileData + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + if let name { + result.append(.text(named: "name", value: name)) + } + + if let sourceURL { + result.append(.text(named: "source_url", value: sourceURL.absoluteString)) + } + + if let sourceLang { + result.append(.text(named: "source_lang", value: sourceLang)) + } + + result.append(.text(named: "target_lang", value: targetLang)) + + if let numSpeakers { + result.append(.text(named: "num_speakers", value: String(numSpeakers))) + } + + if let watermark { + result.append(.text(named: "watermark", value: String(watermark))) + } + + if let startTime { + result.append(.text(named: "start_time", value: String(startTime))) + } + + if let endTime { + result.append(.text(named: "end_time", value: String(endTime))) + } + + if let highestResolution { + result.append(.text(named: "highest_resolution", value: String(highestResolution))) + } + + if let dropBackgroundAudio { + result.append(.text(named: "drop_background_audio", value: String(dropBackgroundAudio))) + } + + if let useProfanityFilter { + result.append(.text(named: "use_profanity_filter", value: String(useProfanityFilter))) + } + + if let fileData { + result.append( + .file( + named: "file", + data: fileData, + filename: "input.mp4", + contentType: .mp4 + ) + ) + } + + return result + } + } + public struct DubbingInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let voiceId: String + public let audioURL: URL + public let languageCode: String + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings + + public init( + voiceId: String, + audioURL: URL, + languageCode: String, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.languageCode = languageCode + self.model = model + self.voiceSettings = voiceSettings + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + result.append( + .text( + named: "model_id", + value: model.rawValue + ) + ) + + result.append( + .text( + named: "language_code", + value: languageCode + ) + ) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { + result.append( + .text( + named: "voice_settings", + value: voiceSettingsString + ) + ) + } + + if let fileData = try? Data(contentsOf: audioURL) { + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg + ) + ) + } + + return result + } + } } } diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift index b80abbd5..658d8de4 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.ResponseBodies.swift @@ -16,5 +16,28 @@ extension ElevenLabs.APISpecification { public struct VoiceID: Codable { public let voiceId: String } + + public struct DubbingResponse: Codable { + public let dubbingId: String + public let expectedDurationSec: Double + } + + public struct DubbingStatus: Codable { + public enum State: String, Codable { + case processing + case completed + case failed + } + + public let state: State + public let failure_reason: String? + public let progress: Double? + } + + public struct DubbingProgress: Codable { + public let status: DubbingStatus + public let expectedDuration: TimeInterval + public let dubbingId: String + } } } diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift index fa442c34..c520694a 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift @@ -94,6 +94,24 @@ extension ElevenLabs { "/v1/voices/\(context.input)" }) var deleteVoice = Endpoint() + + // Dubbing + @POST + @Path("/v1/dubbing") + @Body(multipart: .input) + var initiateDubbing = Endpoint() + + @GET + @Path({ context -> String in + "/v1/dubbing/\(context.input)/status" + }) + var getDubbingStatus = Endpoint() + + @GET + @Path({ context -> String in + "/v1/dubbing/\(context.input)" + }) + var getDubbingResult = Endpoint() } } diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client+Dubbing.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client+Dubbing.swift new file mode 100644 index 00000000..35ca928c --- /dev/null +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client+Dubbing.swift @@ -0,0 +1,83 @@ +// +// ElevenLabs.Client+Dubbing.swift +// AI +// +// Created by Jared Davidson on 1/7/25. +// + +import Foundation + +extension ElevenLabs.Client { + public func dub( + fileData: Data? = nil, + sourceURL: URL? = nil, + name: String? = nil, + sourceLang: String? = nil, + targetLang: String,app + numSpeakers: Int? = nil, + options: DubbingOptions = .init(), + progress: @escaping (DubbingProgress) async -> Void + ) async throws -> DubbingResult { + guard fileData != nil || sourceURL != nil else { + throw NSError(domain: "ElevenLabs", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "Either fileData or sourceURL must be provided" + ]) + } + + let request = ElevenLabs.APISpecification.RequestBodies.DubbingRequest( + name: name, + sourceURL: sourceURL, + sourceLang: sourceLang, + targetLang: targetLang, + numSpeakers: numSpeakers, + watermark: options.watermark, + startTime: options.startTime, + endTime: options.endTime, + highestResolution: options.highestResolution, + dropBackgroundAudio: options.dropBackgroundAudio, + useProfanityFilter: options.useProfanityFilter, + fileData: fileData + ) + + // Start dubbing process + let response = try await run(\.initiateDubbing, with: request) + let dubbingId = response.dubbingId + let expectedDuration = response.expectedDurationSec + + // Poll for status + let pollingInterval: TimeInterval = 5 // seconds + let maxAttempts = Int(ceil(expectedDuration / pollingInterval)) + 10 // Add some buffer attempts + + for _ in 0.. Date: Wed, 8 Jan 2025 18:19:50 -0700 Subject: [PATCH 55/73] update --- Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index 2093a3ea..b33cb1e4 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -42,11 +42,13 @@ extension ElevenLabs.Client { public func speech( for text: String, voiceID: String, + languageCode: String?, voiceSettings: ElevenLabs.VoiceSettings, model: ElevenLabs.Model ) async throws -> Data { let requestBody = ElevenLabs.APISpecification.RequestBodies.SpeechRequest( text: text, + languageCode: languageCode, voiceSettings: voiceSettings, model: model ) @@ -57,12 +59,14 @@ extension ElevenLabs.Client { public func speechToSpeech( inputAudioURL: URL, voiceID: String, + languageCode: String?, voiceSettings: ElevenLabs.VoiceSettings, model: ElevenLabs.Model ) async throws -> Data { let input = ElevenLabs.APISpecification.RequestBodies.SpeechToSpeechInput( voiceId: voiceID, audioURL: inputAudioURL, + languageCode: languageCode, model: model, voiceSettings: voiceSettings ) From 3a90366a560a96b21c636c8086ca42e823bb2254 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 10 Jan 2025 13:03:39 -0700 Subject: [PATCH 56/73] Initial Push w/ Elements from Voice (Not Working) --- .../AbstractVoice.swift | 63 +++++++ .../AbstractVoiceSettings.swift | 147 ++++++++++++++++ .../SpeechSynthesisRequestHandling.swift | 70 ++++++++ .../VideoGenerationSettings.FrameRate.swift | 16 ++ ...deoGenerationSettings.MotionSettings.swift | 23 +++ .../VideoGenerationSettings.Quality.swift | 29 ++++ .../VideoGenerationSettings.Resolution.swift | 163 ++++++++++++++++++ ...ideoGenerationSettings.StyleStrength.swift | 27 +++ .../VideoGenerationSettings.swift | 43 +++++ .../VideoGenerationRequestHandling.swift | 41 +++++ .../VideoModel.swift | 35 ++++ 11 files changed, 657 insertions(+) create mode 100644 Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift create mode 100644 Sources/AI/WIP - Move Somewhere Else/VideoModel.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift b/Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift new file mode 100644 index 00000000..ff7dfc11 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift @@ -0,0 +1,63 @@ +// +// AudioStore.swift +// Voice +// +// Created by Jared Davidson on 10/31/24. +// + +import CorePersistence +import SwiftUI +import AVFoundation +import UniformTypeIdentifiers +import ElevenLabs + +public struct AbstractVoice: Codable, Hashable, Identifiable, Sendable { + public typealias ID = _TypeAssociatedID + + public let id: ID + public let voiceID: String + public let name: String + public let description: String? + + init( + voiceID: String, + name: String, + description: String? + ) { + self.id = .init(rawValue: voiceID) + self.voiceID = voiceID + self.name = name + self.description = description + } +} + +// MARK: - Conformances + +public protocol AbstractVoiceInitiable { + init(voice: AbstractVoice) throws +} + +public protocol AbstractVoiceConvertible { + func __conversion() throws -> AbstractVoice +} + +extension ElevenLabs.Voice: AbstractVoiceConvertible { + public func __conversion() throws -> AbstractVoice { + return AbstractVoice( + voiceID: self.voiceID, + name: self.name, + description: self.description + ) + } +} + +extension ElevenLabs.Voice: AbstractVoiceInitiable { + public init(voice: AbstractVoice) throws { + self.init( + voiceID: voice.voiceID, + name: voice.name, + description: voice.description, + isOwner: nil + ) + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift b/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift new file mode 100644 index 00000000..76052981 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift @@ -0,0 +1,147 @@ +// +// VoiceStore.swift +// Voice +// +// Created by Jared Davidson on 10/30/24. +// + +import SwiftUIZ +import CorePersistence +import ElevenLabs + +public struct AbstractVoiceSettings: Codable, Sendable, Initiable { + public init() { + self.init(stability: 1.0) + } + + + public enum Setting: String, Codable, Sendable { + case stability + case similarityBoost = "similarity_boost" + case styleExaggeration = "style" + case speakerBoost = "use_speaker_boost" + } + + /// Increasing stability will make the voice more consistent between re-generations, but it can also make it sounds a bit monotone. On longer text fragments it is recommended to lower this value. + /// This is a double between 0 (more variable) and 1 (more stable). + public var stability: Double + + /// Increasing the Similarity Boost setting enhances the overall voice clarity and targets speaker similarity. However, very high values can cause artifacts, so it is recommended to adjust this setting to find the optimal value. + /// This is a double between 0 (Low) and 1 (High). + public var similarityBoost: Double + + /// High values are recommended if the style of the speech should be exaggerated compared to the selected voice. Higher values can lead to more instability in the generated speech. Setting this to 0 will greatly increase generation speed and is the default setting. + public var styleExaggeration: Double + + /// Boost the similarity of the synthesized speech and the voice at the cost of some generation speed. + public var speakerBoost: Bool + + public var removeBackgroundNoise: Bool + + public init(stability: Double, + similarityBoost: Double, + styleExaggeration: Double, + speakerBoost: Bool, + removeBackgroundNoise: Bool) { + self.stability = max(0, min(1, stability)) + self.similarityBoost = max(0, min(1, similarityBoost)) + self.styleExaggeration = max(0, min(1, styleExaggeration)) + self.speakerBoost = speakerBoost + self.removeBackgroundNoise = removeBackgroundNoise + } + + public init(stability: Double? = nil, + similarityBoost: Double? = nil, + styleExaggeration: Double? = nil, + speakerBoost: Bool? = nil, + removeBackgroundNoise: Bool? = nil) { + self.stability = stability.map { max(0, min(1, $0)) } ?? 0.5 + self.similarityBoost = similarityBoost.map { max(0, min(1, $0)) } ?? 0.75 + self.styleExaggeration = styleExaggeration.map { max(0, min(1, $0)) } ?? 0 + self.speakerBoost = speakerBoost ?? true + self.removeBackgroundNoise = removeBackgroundNoise ?? false + } + + public init(stability: Double) { + self.init( + stability: stability, + similarityBoost: 0.75, + styleExaggeration: 0, + speakerBoost: true, + removeBackgroundNoise: false + ) + } + + public init(similarityBoost: Double) { + self.init( + stability: 0.5, + similarityBoost: similarityBoost, + styleExaggeration: 0, + speakerBoost: true, + removeBackgroundNoise: false + ) + } + + public init(styleExaggeration: Double) { + self.init( + stability: 0.5, + similarityBoost: 0.75, + styleExaggeration: styleExaggeration, + speakerBoost: true, + removeBackgroundNoise: false + ) + } + + public init(speakerBoost: Bool) { + self.init( + stability: 0.5, + similarityBoost: 0.75, + styleExaggeration: 0, + speakerBoost: speakerBoost, + removeBackgroundNoise: false + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(stability, forKey: .stability) + try container.encode(similarityBoost, forKey: .similarityBoost) + try container.encode(styleExaggeration, forKey: .styleExaggeration) + try container.encode(speakerBoost, forKey: .speakerBoost) + try container.encode(removeBackgroundNoise, forKey: .removeBackgroundNoise) + } +} + + +public protocol AbstractVoiceSettingsInitiable { + init(settings: AbstractVoiceSettings) throws +} + +public protocol AbstractVoiceSettingsConvertible { + func __conversion() throws -> AbstractVoiceSettings +} + +extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsConvertible { + public func __conversion() throws -> AbstractVoiceSettings { + return .init( + stability: stability, + similarityBoost: similarityBoost, + styleExaggeration: styleExaggeration, + speakerBoost: speakerBoost, + removeBackgroundNoise: removeBackgroundNoise + ) + } +} + +extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsInitiable { + public init(settings: AbstractVoiceSettings) throws { + self.init( + stability: settings.stability, + similarityBoost: settings.similarityBoost, + styleExaggeration: settings.styleExaggeration, + speakerBoost: settings.speakerBoost, + removeBackgroundNoise: settings.removeBackgroundNoise + ) + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..43ac0d9c --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -0,0 +1,70 @@ +// +// SpeechSynthesisRequestHandling.swift +// Voice +// +// Created by Jared Davidson on 10/30/24. +// + +import Foundation +import AI +import ElevenLabs +import PlayHT +import SwiftUI + +public protocol SpeechToSpeechRequest { + +} + +public protocol SpeechToSpeechRequestHandling { + +} + +public protocol SpeechSynthesisRequestHandling { + func availableVoices() async throws -> [ElevenLabs.Voice] + + func speech( + for text: String, + voiceID: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) async throws -> Data + + func speechToSpeech( + inputAudioURL: URL, + voiceID: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) async throws -> Data + + func upload( + voiceWithName name: String, + description: String, + fileURL: URL + ) async throws -> ElevenLabs.Voice.ID + + func edit( + voice: ElevenLabs.Voice.ID, + name: String, + description: String, + fileURL: URL? + ) async throws -> Bool + + func delete(voice: ElevenLabs.Voice.ID) async throws +} + +// MARK: - Environment Key + +private struct ElevenLabsClientKey: EnvironmentKey { + static let defaultValue: (any SpeechSynthesisRequestHandling)? = ElevenLabs.Client(apiKey: "") +} + +extension EnvironmentValues { + var speechSynthesizer: (any SpeechSynthesisRequestHandling)? { + get { self[ElevenLabsClientKey.self] } + set { self[ElevenLabsClientKey.self] = newValue } + } +} + +// MARK: - Conformances + +extension ElevenLabs.Client: SpeechSynthesisRequestHandling {} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift new file mode 100644 index 00000000..da61c5dd --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift @@ -0,0 +1,16 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +extension VideoGenerationSettings { + public enum FrameRate: Int, Codable, CaseIterable { + case fps8 = 8 + case fps16 = 16 + case fps24 = 24 + case fps30 = 30 + + public var fps: Int { rawValue } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift new file mode 100644 index 00000000..addec423 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +extension VideoGenerationSettings { + public struct MotionSettings: Codable, Hashable { + public var stabilize: Bool + public var motionBucketId: Int // 0-127 + public var conditioningAugmentation: Double // 0.01-0.1 + + public init( + stabilize: Bool = true, + motionBucketId: Int = 127, + conditioningAugmentation: Double = 0.02 + ) { + self.stabilize = stabilize + self.motionBucketId = max(0, min(127, motionBucketId)) + self.conditioningAugmentation = max(0.01, min(0.1, conditioningAugmentation)) + } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift new file mode 100644 index 00000000..bc3516a1 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +extension VideoGenerationSettings { + public enum Quality: String, Codable, CaseIterable { + case draft = "draft" // 20 steps + case fast = "fast" // 30 steps + case balanced = "balanced" // 35 steps + case quality = "quality" // 40 steps + case max = "max" // 50 steps + + var inferenceSteps: Int { + switch self { + case .draft: return 20 + case .fast: return 30 + case .balanced: return 35 + case .quality: return 40 + case .max: return 50 + } + } + + var qualityValue: Double { + Double(inferenceSteps - 20) / 30 + } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift new file mode 100644 index 00000000..a140a046 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift @@ -0,0 +1,163 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +extension VideoGenerationSettings { + public enum Resolution: Codable, Hashable { + // Square Resolutions + case sd512x512 + case sd768x768 + case sd1024x1024 + + // Landscape HD Resolutions + case hd720p // 1280x720 + case hd1080p // 1920x1080 + case hd1440p // 2560x1440 + case uhd4k // 3840x2160 + + // Social Media Formats + case instagram // 1080x1080 + case story // 1080x1920 + case tiktok // 1080x1920 + case youtube // 1920x1080 + + // Custom Resolution + case custom(width: Int, height: Int) + + public static var allCases: [Resolution] { + [ + .sd512x512, .sd768x768, .sd1024x1024, + .hd720p, .hd1080p, .hd1440p, .uhd4k, + .instagram, .story, .tiktok, .youtube + ] + } + + public var dimensions: (width: Int, height: Int) { + switch self { + // Square Resolutions + case .sd512x512: + return (512, 512) + case .sd768x768: + return (768, 768) + case .sd1024x1024: + return (1024, 1024) + + // Landscape HD Resolutions + case .hd720p: + return (1280, 720) + case .hd1080p: + return (1920, 1080) + case .hd1440p: + return (2560, 1440) + case .uhd4k: + return (3840, 2160) + + // Social Media Formats + case .instagram: + return (1080, 1080) + case .story: + return (1080, 1920) + case .tiktok: + return (1080, 1920) + case .youtube: + return (1920, 1080) + + case .custom(let width, let height): + return (width, height) + } + } + + public var width: Int { dimensions.width } + public var height: Int { dimensions.height } + + public var aspectRatio: String { + let gcd = calculateGCD(width, height) + let simplifiedWidth = width / gcd + let simplifiedHeight = height / gcd + + // Check for common aspect ratios + switch (simplifiedWidth, simplifiedHeight) { + case (1, 1): return "1:1" // Square + case (16, 9): return "16:9" // Standard Widescreen + case (9, 16): return "9:16" // Vertical/Portrait + case (4, 3): return "4:3" // Traditional TV + case (21, 9): return "21:9" // Ultrawide + default: return "\(simplifiedWidth):\(simplifiedHeight)" + } + } + + public var resolution: String { + switch self { + case .uhd4k: + return "4K" + case .hd1440p: + return "1440p" + case .hd1080p, .youtube: + return "1080p" + case .hd720p: + return "720p" + case .instagram, .story, .tiktok: + return "1080p" + case .sd512x512: + return "512p" + case .sd768x768: + return "768p" + case .sd1024x1024: + return "1024p" + case .custom(let width, _): + if width >= 3840 { return "4K" } + if width >= 2560 { return "1440p" } + if width >= 1920 { return "1080p" } + if width >= 1280 { return "720p" } + return "\(width)p" + } + } + + public static func detectResolution(width: Int, height: Int) -> Resolution { + switch (width, height) { + case (512, 512): return .sd512x512 + case (768, 768): return .sd768x768 + case (1024, 1024): return .sd1024x1024 + case (1280, 720): return .hd720p + case (1920, 1080): return .hd1080p + case (2560, 1440): return .hd1440p + case (3840, 2160): return .uhd4k + case (1080, 1080): return .instagram + case (1080, 1920): return .story + default: return .custom(width: width, height: height) + } + } + + private func calculateGCD(_ a: Int, _ b: Int) -> Int { + var a = a + var b = b + while b != 0 { + let temp = b + b = a % b + a = temp + } + + return a + } + + public var displayName: String { + switch self { + case .sd512x512: return "512×512" + case .sd768x768: return "768×768" + case .sd1024x1024: return "1024×1024" + case .hd720p: return "HD 720p" + case .hd1080p: return "Full HD 1080p" + case .hd1440p: return "QHD 1440p" + case .uhd4k: return "4K UHD" + case .instagram: return "Instagram Square" + case .story: return "Instagram/TikTok Story" + case .tiktok: return "TikTok Video" + case .youtube: return "YouTube HD" + case .custom(let width, let height): + return "\(width)×\(height)" + } + } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift new file mode 100644 index 00000000..6a75ff9b --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +extension VideoGenerationSettings { + public enum StyleStrength: String, Codable, CaseIterable { + case subtle = "subtle" // 1-5 + case balanced = "balanced" // 5-10 + case strong = "strong" // 10-15 + case extreme = "extreme" // 15-20 + + var guidanceScale: Double { + switch self { + case .subtle: return 3.0 + case .balanced: return 7.5 + case .strong: return 12.5 + case .extreme: return 17.5 + } + } + + var strengthValue: Double { + (guidanceScale - 1) / 19 + } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift new file mode 100644 index 00000000..1c8d7314 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import Foundation + +public struct VideoGenerationSettings: Codable, Hashable { + /// Duration of the generated video in seconds (1-60) + public var duration: Double { + didSet { + duration = max(1, min(60, duration)) + } + } + + public var resolution: Resolution + public var frameRate: FrameRate + public var quality: Quality + public var styleStrength: StyleStrength + public var motion: MotionSettings + public var negativePrompt: String + + public var fps: Int { frameRate.fps } + public var numInferenceSteps: Int { quality.inferenceSteps } + public var guidanceScale: Double { styleStrength.guidanceScale } + + public init( + duration: Double = 10.0, + resolution: Resolution = .sd512x512, + frameRate: FrameRate = .fps24, + quality: Quality = .balanced, + styleStrength: StyleStrength = .balanced, + motion: MotionSettings = MotionSettings(), + negativePrompt: String = "" + ) { + self.duration = max(1, min(60, duration)) + self.resolution = resolution + self.frameRate = frameRate + self.quality = quality + self.styleStrength = styleStrength + self.motion = motion + self.negativePrompt = negativePrompt + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift new file mode 100644 index 00000000..f8ea8566 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import AVFoundation +import Foundation +import SwiftUI + +public protocol VideoGenerationRequestHandling { + func availableModels() async throws -> [VideoModel] + + func textToVideo( + text: String, + model: VideoModel, + settings: VideoGenerationSettings + ) async throws -> Data + + func imageToVideo( + imageURL: URL, + model: VideoModel, + settings: VideoGenerationSettings + ) async throws -> Data + + func videoToVideo( + videoURL: URL, + prompt: String, + model: VideoModel, + settings: VideoGenerationSettings + ) async throws -> Data +} + +private struct VideoGeneratorKey: EnvironmentKey { + static let defaultValue: (any VideoGenerationRequestHandling)? = DummyVideoGenerator() +} + +extension EnvironmentValues { + var videoClient: (any VideoGenerationRequestHandling)? { + get { self[VideoGeneratorKey.self] } + set { self[VideoGeneratorKey.self] = newValue } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoModel.swift b/Sources/AI/WIP - Move Somewhere Else/VideoModel.swift new file mode 100644 index 00000000..ed63bae8 --- /dev/null +++ b/Sources/AI/WIP - Move Somewhere Else/VideoModel.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) Preternatural AI, Inc. +// + +import CorePersistence +import Foundation + +public struct VideoModel: Codable, Hashable, Identifiable { + public typealias ID = _TypeAssociatedID + + public let id: ID + public let endpoint: String + public let name: String + public let description: String? + public let capabilities: [Capability] + + public enum Capability: String, Codable { + case textToVideo + case imageToVideo + case videoToVideo + } + + public init( + endpoint: String, + name: String, + description: String?, + capabilities: [Capability] + ) { + self.id = .random() + self.endpoint = endpoint + self.name = name + self.description = description + self.capabilities = capabilities + } +} From 7541d4d644f1f86ae9ebfa933f274c74f2a4cce3 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 10 Jan 2025 13:32:50 -0700 Subject: [PATCH 57/73] Building again --- .../AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift | 2 +- .../SpeechSynthesisRequestHandling.swift | 2 +- .../VideoGenerationSettings.Quality.swift | 4 ++-- .../VideoGenerationSettings.StyleStrength.swift | 4 ++-- .../Video Generation Setttings/VideoGenerationSettings.swift | 2 +- .../VideoGenerationRequestHandling.swift | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift b/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift index 76052981..d6f87dc5 100644 --- a/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift +++ b/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift @@ -9,7 +9,7 @@ import SwiftUIZ import CorePersistence import ElevenLabs -public struct AbstractVoiceSettings: Codable, Sendable, Initiable { +public struct AbstractVoiceSettings: Codable, Sendable, Initiable, Equatable { public init() { self.init(stability: 1.0) } diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift index 43ac0d9c..d6e9f590 100644 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -59,7 +59,7 @@ private struct ElevenLabsClientKey: EnvironmentKey { } extension EnvironmentValues { - var speechSynthesizer: (any SpeechSynthesisRequestHandling)? { + public var speechSynthesizer: (any SpeechSynthesisRequestHandling)? { get { self[ElevenLabsClientKey.self] } set { self[ElevenLabsClientKey.self] = newValue } } diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift index bc3516a1..5ce0de27 100644 --- a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift @@ -12,7 +12,7 @@ extension VideoGenerationSettings { case quality = "quality" // 40 steps case max = "max" // 50 steps - var inferenceSteps: Int { + public var inferenceSteps: Int { switch self { case .draft: return 20 case .fast: return 30 @@ -22,7 +22,7 @@ extension VideoGenerationSettings { } } - var qualityValue: Double { + public var qualityValue: Double { Double(inferenceSteps - 20) / 30 } } diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift index 6a75ff9b..1fdc10af 100644 --- a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift @@ -11,7 +11,7 @@ extension VideoGenerationSettings { case strong = "strong" // 10-15 case extreme = "extreme" // 15-20 - var guidanceScale: Double { + public var guidanceScale: Double { switch self { case .subtle: return 3.0 case .balanced: return 7.5 @@ -20,7 +20,7 @@ extension VideoGenerationSettings { } } - var strengthValue: Double { + public var strengthValue: Double { (guidanceScale - 1) / 19 } } diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift index 1c8d7314..81a72cfb 100644 --- a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift +++ b/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift @@ -4,7 +4,7 @@ import Foundation -public struct VideoGenerationSettings: Codable, Hashable { +public struct VideoGenerationSettings: Codable, Hashable, Equatable { /// Duration of the generated video in seconds (1-60) public var duration: Double { didSet { diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift index f8ea8566..9a6ed8e8 100644 --- a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift @@ -30,11 +30,11 @@ public protocol VideoGenerationRequestHandling { } private struct VideoGeneratorKey: EnvironmentKey { - static let defaultValue: (any VideoGenerationRequestHandling)? = DummyVideoGenerator() + public static let defaultValue: (any VideoGenerationRequestHandling)? = nil } extension EnvironmentValues { - var videoClient: (any VideoGenerationRequestHandling)? { + public var videoClient: (any VideoGenerationRequestHandling)? { get { self[VideoGeneratorKey.self] } set { self[VideoGeneratorKey.self] = newValue } } From 9497758347534aadcba256c68f720263f45383bf Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 10 Jan 2025 17:29:29 -0700 Subject: [PATCH 58/73] Working --- .../SpeechSynthesisRequestHandling.swift | 30 ++++++++++++++++++- .../VideoGenerationRequestHandling.swift | 28 +++++++++++++++++ .../API/ElevenLabs.APISpecification.swift | 3 -- .../Intramodular/ElevenLabs.Client.swift | 25 +++++++++++++++- 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift index d6e9f590..54f9e853 100644 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -19,7 +19,7 @@ public protocol SpeechToSpeechRequestHandling { } -public protocol SpeechSynthesisRequestHandling { +public protocol SpeechSynthesisRequestHandling: AnyObject { func availableVoices() async throws -> [ElevenLabs.Voice] func speech( @@ -68,3 +68,31 @@ extension EnvironmentValues { // MARK: - Conformances extension ElevenLabs.Client: SpeechSynthesisRequestHandling {} + + +public struct AnySpeechSynthesisRequestHandling: Hashable { + private let _service: any CoreMI._ServiceClientProtocol + private let _base: any SpeechSynthesisRequestHandling + private let _hashValue: Int + + public init( + _ base: any SpeechSynthesisRequestHandling, + service: any CoreMI._ServiceClientProtocol + ) { + self._base = base + self._hashValue = ObjectIdentifier(base as AnyObject).hashValue + self._service = service + } + + public static func == (lhs: AnySpeechSynthesisRequestHandling, rhs: AnySpeechSynthesisRequestHandling) -> Bool { + lhs._hashValue == rhs._hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(_hashValue) + } + + public func base() -> any SpeechSynthesisRequestHandling { + _base + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift index 9a6ed8e8..8d975397 100644 --- a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift @@ -5,6 +5,7 @@ import AVFoundation import Foundation import SwiftUI +import LargeLanguageModels public protocol VideoGenerationRequestHandling { func availableModels() async throws -> [VideoModel] @@ -39,3 +40,30 @@ extension EnvironmentValues { set { self[VideoGeneratorKey.self] = newValue } } } + +public struct AnyVideoGenerationRequestHandling: Hashable { + private let _service: any CoreMI._ServiceClientProtocol + private let _base: any VideoGenerationRequestHandling + private let _hashValue: Int + + public init( + _ base: any VideoGenerationRequestHandling, + service: any CoreMI._ServiceClientProtocol + ) { + self._base = base + self._hashValue = ObjectIdentifier(base as AnyObject).hashValue + self._service = service + } + + public static func == (lhs: AnyVideoGenerationRequestHandling, rhs: AnyVideoGenerationRequestHandling) -> Bool { + lhs._hashValue == rhs._hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(_hashValue) + } + + public func base() -> any VideoGenerationRequestHandling { + _base + } +} diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift index fa442c34..656255b8 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.swift @@ -128,9 +128,6 @@ extension ElevenLabs.APISpecification { context: DecodeOutputContext ) throws -> Output { do { - if Input.self == RequestBodies.EditVoiceInput.self { - print("TEsts") - } try response.validate() } catch { let apiError: Error diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index 2093a3ea..6fbc4922 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -10,10 +10,15 @@ import SwiftAPI import Merge import FoundationX import Swallow +import LargeLanguageModels extension ElevenLabs { @RuntimeDiscoverable public final class Client: SwiftAPI.Client, ObservableObject { + public static var persistentTypeRepresentation: some IdentityRepresentation { + CoreMI._ServiceVendorIdentifier._ElevenLabs + } + public typealias API = ElevenLabs.APISpecification public typealias Session = HTTPSession @@ -33,6 +38,25 @@ extension ElevenLabs { } } +extension ElevenLabs.Client: CoreMI._ServiceClientProtocol { + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._ElevenLabs else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } +} + extension ElevenLabs.Client { public func availableVoices() async throws -> [ElevenLabs.Voice] { try await run(\.listVoices).voices @@ -50,7 +74,6 @@ extension ElevenLabs.Client { voiceSettings: voiceSettings, model: model ) - return try await run(\.textToSpeech, with: .init(voiceId: voiceID, requestBody: requestBody)) } From fc6e2725c9679513e5665856498d9856dbbf3e00 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 10 Jan 2025 19:11:01 -0700 Subject: [PATCH 59/73] Fixes --- .../SpeechSynthesisRequestHandling.swift | 23 +++++++++++-------- .../VideoGenerationRequestHandling.swift | 22 ++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift index 54f9e853..725b9e86 100644 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -71,17 +71,24 @@ extension ElevenLabs.Client: SpeechSynthesisRequestHandling {} public struct AnySpeechSynthesisRequestHandling: Hashable { - private let _service: any CoreMI._ServiceClientProtocol - private let _base: any SpeechSynthesisRequestHandling private let _hashValue: Int + public let base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling + + public var displayName: String { + switch base { + case is ElevenLabs.Client: + return "ElevenLabs" + default: + fatalError() + } + } + public init( - _ base: any SpeechSynthesisRequestHandling, - service: any CoreMI._ServiceClientProtocol + _ base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling ) { - self._base = base + self.base = base self._hashValue = ObjectIdentifier(base as AnyObject).hashValue - self._service = service } public static func == (lhs: AnySpeechSynthesisRequestHandling, rhs: AnySpeechSynthesisRequestHandling) -> Bool { @@ -91,8 +98,4 @@ public struct AnySpeechSynthesisRequestHandling: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(_hashValue) } - - public func base() -> any SpeechSynthesisRequestHandling { - _base - } } diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift index 8d975397..d9f9ab1c 100644 --- a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift @@ -42,17 +42,23 @@ extension EnvironmentValues { } public struct AnyVideoGenerationRequestHandling: Hashable { - private let _service: any CoreMI._ServiceClientProtocol - private let _base: any VideoGenerationRequestHandling + public let base: any CoreMI._ServiceClientProtocol & VideoGenerationRequestHandling private let _hashValue: Int + +// var displayName: String { +// switch base { +// case is FalVideoGenerationRequestHandling: +// return "Fal" +// default: +// fatalError() +// } +// } public init( - _ base: any VideoGenerationRequestHandling, - service: any CoreMI._ServiceClientProtocol + _ base: any CoreMI._ServiceClientProtocol & VideoGenerationRequestHandling ) { - self._base = base + self.base = base self._hashValue = ObjectIdentifier(base as AnyObject).hashValue - self._service = service } public static func == (lhs: AnyVideoGenerationRequestHandling, rhs: AnyVideoGenerationRequestHandling) -> Bool { @@ -62,8 +68,4 @@ public struct AnyVideoGenerationRequestHandling: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(_hashValue) } - - public func base() -> any VideoGenerationRequestHandling { - _base - } } From fbc2f1062a265cbea60e5e9c35ae258ebc87f8f7 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Mon, 13 Jan 2025 16:11:30 +0530 Subject: [PATCH 60/73] Update --- .../SpeechSynthesisRequestHandling.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift index 725b9e86..1de670e4 100644 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -8,7 +8,6 @@ import Foundation import AI import ElevenLabs -import PlayHT import SwiftUI public protocol SpeechToSpeechRequest { From f74b0f59ef655540d6f6136dea8872455f758d42 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 14 Jan 2025 16:11:00 -0700 Subject: [PATCH 61/73] Move files to LargeLanguageModels. Added conformances to individual structs. --- Package.swift | 2 +- .../AnySpeechSynthesisRequestHandling.swift | 39 +++++++ .../SpeechSynthesisRequestHandling.swift | 100 ------------------ .../Intramodular/ElevenLabs.Client.swift | 53 ++++++++++ .../Intramodular/ElevenLabs.Voice.swift | 22 ++++ .../ElevenLabs.VoiceSettings.swift | 27 +++++ .../AbstractVoice.swift | 24 +---- .../AbstractVoiceSettings.swift | 25 ----- .../SpeechSynthesisRequestHandling.swift | 63 +++++++++++ .../VideoGenerationSettings.FrameRate.swift | 0 ...deoGenerationSettings.MotionSettings.swift | 0 .../VideoGenerationSettings.Quality.swift | 0 .../VideoGenerationSettings.Resolution.swift | 0 ...ideoGenerationSettings.StyleStrength.swift | 0 .../VideoGenerationSettings.swift | 0 .../VideoGenerationRequestHandling.swift | 0 .../VideoModel.swift | 0 .../Intramodular/Models/PlayHT.Voice.swift | 57 +++++++++- .../Rime/Intramodular/Models/Rime.Voice.swift | 47 ++++++++ 19 files changed, 309 insertions(+), 150 deletions(-) create mode 100644 Sources/AI/AnySpeechSynthesisRequestHandling.swift delete mode 100644 Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/AbstractVoice.swift (60%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/AbstractVoiceSettings.swift (83%) create mode 100644 Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift (100%) rename Sources/{AI => LargeLanguageModels/Intramodular}/WIP - Move Somewhere Else/VideoModel.swift (100%) diff --git a/Package.swift b/Package.swift index a374aaf0..f731cfd7 100644 --- a/Package.swift +++ b/Package.swift @@ -115,7 +115,7 @@ let package = Package( "Merge", "NetworkKit", "Swallow", - "SwiftUIX", + "SwiftUIX" ], path: "Sources/LargeLanguageModels", resources: [ diff --git a/Sources/AI/AnySpeechSynthesisRequestHandling.swift b/Sources/AI/AnySpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..18dce27e --- /dev/null +++ b/Sources/AI/AnySpeechSynthesisRequestHandling.swift @@ -0,0 +1,39 @@ +// +// AnySpeechSynthesisRequestHandling.swift +// AI +// +// Created by Jared Davidson on 1/14/25. +// + +import ElevenLabs +import LargeLanguageModels + +public struct AnySpeechSynthesisRequestHandling: Hashable { + private let _hashValue: Int + + public let base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling + + public var displayName: String { + switch base { + case is ElevenLabs.Client: + return "ElevenLabs" + default: + fatalError() + } + } + + public init( + _ base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling + ) { + self.base = base + self._hashValue = ObjectIdentifier(base as AnyObject).hashValue + } + + public static func == (lhs: AnySpeechSynthesisRequestHandling, rhs: AnySpeechSynthesisRequestHandling) -> Bool { + lhs._hashValue == rhs._hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(_hashValue) + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift deleted file mode 100644 index 1de670e4..00000000 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// SpeechSynthesisRequestHandling.swift -// Voice -// -// Created by Jared Davidson on 10/30/24. -// - -import Foundation -import AI -import ElevenLabs -import SwiftUI - -public protocol SpeechToSpeechRequest { - -} - -public protocol SpeechToSpeechRequestHandling { - -} - -public protocol SpeechSynthesisRequestHandling: AnyObject { - func availableVoices() async throws -> [ElevenLabs.Voice] - - func speech( - for text: String, - voiceID: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) async throws -> Data - - func speechToSpeech( - inputAudioURL: URL, - voiceID: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) async throws -> Data - - func upload( - voiceWithName name: String, - description: String, - fileURL: URL - ) async throws -> ElevenLabs.Voice.ID - - func edit( - voice: ElevenLabs.Voice.ID, - name: String, - description: String, - fileURL: URL? - ) async throws -> Bool - - func delete(voice: ElevenLabs.Voice.ID) async throws -} - -// MARK: - Environment Key - -private struct ElevenLabsClientKey: EnvironmentKey { - static let defaultValue: (any SpeechSynthesisRequestHandling)? = ElevenLabs.Client(apiKey: "") -} - -extension EnvironmentValues { - public var speechSynthesizer: (any SpeechSynthesisRequestHandling)? { - get { self[ElevenLabsClientKey.self] } - set { self[ElevenLabsClientKey.self] = newValue } - } -} - -// MARK: - Conformances - -extension ElevenLabs.Client: SpeechSynthesisRequestHandling {} - - -public struct AnySpeechSynthesisRequestHandling: Hashable { - private let _hashValue: Int - - public let base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling - - public var displayName: String { - switch base { - case is ElevenLabs.Client: - return "ElevenLabs" - default: - fatalError() - } - } - - public init( - _ base: any CoreMI._ServiceClientProtocol & SpeechSynthesisRequestHandling - ) { - self.base = base - self._hashValue = ObjectIdentifier(base as AnyObject).hashValue - } - - public static func == (lhs: AnySpeechSynthesisRequestHandling, rhs: AnySpeechSynthesisRequestHandling) -> Bool { - lhs._hashValue == rhs._hashValue - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(_hashValue) - } -} diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift index 6fbc4922..2d7a03b6 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Client.swift @@ -130,3 +130,56 @@ extension ElevenLabs.Client { try await run(\.deleteVoice, with: voice.rawValue) } } + +// MARK: - Conformances + +extension ElevenLabs.Client: SpeechSynthesisRequestHandling { + public func availableVoices() async throws -> [AbstractVoice] { + return try await self.availableVoices().map({try $0.__conversion()}) + } + + public func speech(for text: String, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + try await self.speech( + for: text, + voiceID: voiceID, + voiceSettings: .init(settings: voiceSettings), + model: .init(rawValue: model) ?? .MultilingualV1 + ) + } + + public func speechToSpeech(inputAudioURL: URL, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + try await self.speechToSpeech( + inputAudioURL: inputAudioURL, + voiceID: voiceID, + voiceSettings: .init(settings: voiceSettings), + model: .init(rawValue: model) ?? .MultilingualV1 + ) + } + + public func upload(voiceWithName name: String, description: String, fileURL: URL) async throws -> AbstractVoice.ID { + let voice: ElevenLabs.Voice.ID = try await self.upload( + voiceWithName: name, + description: description, + fileURL: fileURL + ) + + return .init(rawValue: voice.rawValue) + } + + public func edit(voice: AbstractVoice.ID, name: String, description: String, fileURL: URL?) async throws -> Bool { + try await self.edit( + voice: ElevenLabs.Voice.ID(rawValue: voice.rawValue), + name: name, + description: description, + fileURL: fileURL + ) + } + + public func delete(voice: AbstractVoice.ID) async throws { + try await self.delete( + voice: ElevenLabs.Voice.ID( + rawValue: voice.rawValue + ) + ) + } +} diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift index 3a54532f..dbe29d63 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.Voice.swift @@ -4,6 +4,7 @@ import Foundation import Swift +import LargeLanguageModels extension ElevenLabs { public struct Voice: Hashable, Identifiable, Sendable { @@ -42,3 +43,24 @@ extension ElevenLabs.Voice: Codable { case isOwner } } + +extension ElevenLabs.Voice: AbstractVoiceConvertible { + public func __conversion() throws -> AbstractVoice { + return AbstractVoice( + voiceID: self.voiceID, + name: self.name, + description: self.description + ) + } +} + +extension ElevenLabs.Voice: AbstractVoiceInitiable { + public init(voice: AbstractVoice) throws { + self.init( + voiceID: voice.voiceID, + name: voice.name, + description: voice.description, + isOwner: nil + ) + } +} diff --git a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift index 1ffb7947..f0a6b825 100644 --- a/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift +++ b/Sources/ElevenLabs/Intramodular/ElevenLabs.VoiceSettings.swift @@ -3,6 +3,7 @@ // import Foundation +import LargeLanguageModels extension ElevenLabs { public struct VoiceSettings: Codable, Sendable, Hashable { @@ -98,3 +99,29 @@ extension ElevenLabs.VoiceSettings { ) } } + +// MARK: - Conformances + +extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsConvertible { + public func __conversion() throws -> AbstractVoiceSettings { + return .init( + stability: stability, + similarityBoost: similarityBoost, + styleExaggeration: styleExaggeration, + speakerBoost: speakerBoost, + removeBackgroundNoise: removeBackgroundNoise + ) + } +} + +extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsInitiable { + public init(settings: AbstractVoiceSettings) throws { + self.init( + stability: settings.stability, + similarityBoost: settings.similarityBoost, + styleExaggeration: settings.styleExaggeration, + speakerBoost: settings.speakerBoost, + removeBackgroundNoise: settings.removeBackgroundNoise + ) + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoice.swift similarity index 60% rename from Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoice.swift index ff7dfc11..80de4232 100644 --- a/Sources/AI/WIP - Move Somewhere Else/AbstractVoice.swift +++ b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoice.swift @@ -9,7 +9,6 @@ import CorePersistence import SwiftUI import AVFoundation import UniformTypeIdentifiers -import ElevenLabs public struct AbstractVoice: Codable, Hashable, Identifiable, Sendable { public typealias ID = _TypeAssociatedID @@ -19,7 +18,7 @@ public struct AbstractVoice: Codable, Hashable, Identifiable, Sendable { public let name: String public let description: String? - init( + public init( voiceID: String, name: String, description: String? @@ -40,24 +39,3 @@ public protocol AbstractVoiceInitiable { public protocol AbstractVoiceConvertible { func __conversion() throws -> AbstractVoice } - -extension ElevenLabs.Voice: AbstractVoiceConvertible { - public func __conversion() throws -> AbstractVoice { - return AbstractVoice( - voiceID: self.voiceID, - name: self.name, - description: self.description - ) - } -} - -extension ElevenLabs.Voice: AbstractVoiceInitiable { - public init(voice: AbstractVoice) throws { - self.init( - voiceID: voice.voiceID, - name: voice.name, - description: voice.description, - isOwner: nil - ) - } -} diff --git a/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoiceSettings.swift similarity index 83% rename from Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoiceSettings.swift index d6f87dc5..34fd03b6 100644 --- a/Sources/AI/WIP - Move Somewhere Else/AbstractVoiceSettings.swift +++ b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoiceSettings.swift @@ -7,7 +7,6 @@ import SwiftUIZ import CorePersistence -import ElevenLabs public struct AbstractVoiceSettings: Codable, Sendable, Initiable, Equatable { public init() { @@ -121,27 +120,3 @@ public protocol AbstractVoiceSettingsInitiable { public protocol AbstractVoiceSettingsConvertible { func __conversion() throws -> AbstractVoiceSettings } - -extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsConvertible { - public func __conversion() throws -> AbstractVoiceSettings { - return .init( - stability: stability, - similarityBoost: similarityBoost, - styleExaggeration: styleExaggeration, - speakerBoost: speakerBoost, - removeBackgroundNoise: removeBackgroundNoise - ) - } -} - -extension ElevenLabs.VoiceSettings: AbstractVoiceSettingsInitiable { - public init(settings: AbstractVoiceSettings) throws { - self.init( - stability: settings.stability, - similarityBoost: settings.similarityBoost, - styleExaggeration: settings.styleExaggeration, - speakerBoost: settings.speakerBoost, - removeBackgroundNoise: settings.removeBackgroundNoise - ) - } -} diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..e8bec05c --- /dev/null +++ b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -0,0 +1,63 @@ +// +// SpeechSynthesisRequestHandling.swift +// Voice +// +// Created by Jared Davidson on 10/30/24. +// + +import Foundation +import SwiftUI + +public protocol SpeechToSpeechRequest { + +} + +public protocol SpeechToSpeechRequestHandling { + +} + +public protocol SpeechSynthesisRequestHandling: AnyObject { + func availableVoices() async throws -> [AbstractVoice] + + func speech( + for text: String, + voiceID: String, + voiceSettings: AbstractVoiceSettings, + model: String + ) async throws -> Data + + func speechToSpeech( + inputAudioURL: URL, + voiceID: String, + voiceSettings: AbstractVoiceSettings, + model: String + ) async throws -> Data + + func upload( + voiceWithName name: String, + description: String, + fileURL: URL + ) async throws -> AbstractVoice.ID + + func edit( + voice: AbstractVoice.ID, + name: String, + description: String, + fileURL: URL? + ) async throws -> Bool + + func delete(voice: AbstractVoice.ID) async throws +} + +// MARK: - Environment Key + +private struct AbstractClientKey: EnvironmentKey { + static let defaultValue: (any SpeechSynthesisRequestHandling)? = nil +} + +extension EnvironmentValues { + public var speechSynthesizer: (any SpeechSynthesisRequestHandling)? { + get { self[AbstractClientKey.self] } + set { self[AbstractClientKey.self] = newValue } + } +} diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift diff --git a/Sources/AI/WIP - Move Somewhere Else/VideoModel.swift b/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoModel.swift similarity index 100% rename from Sources/AI/WIP - Move Somewhere Else/VideoModel.swift rename to Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoModel.swift diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift index 3ac8907f..7ee76f26 100644 --- a/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.Voice.swift @@ -7,6 +7,7 @@ import Foundation import Swallow +import LargeLanguageModels extension PlayHT { public struct Voice: Codable, Hashable, Identifiable { @@ -16,7 +17,7 @@ extension PlayHT { public let name: String public let language: String? public let languageCode: String? - public let voiceEngine: String + public let voiceEngine: String? public let isCloned: Bool? public let gender: String? public let accent: String? @@ -26,6 +27,39 @@ extension PlayHT { public let texture: String? public let loudness: String? public let tempo: String? + + + init( + id: ID, + name: String, + language: String? = nil, + languageCode: String? = nil, + voiceEngine: String? = nil, + isCloned: Bool? = nil, + gender: String? = nil, + accent: String? = nil, + age: String? = nil, + style: String? = nil, + sample: String? = nil, + texture: String? = nil, + loudness: String? = nil, + tempo: String? = nil + ) { + self.id = id + self.name = name + self.language = language + self.languageCode = languageCode + self.voiceEngine = voiceEngine + self.isCloned = isCloned + self.gender = gender + self.accent = accent + self.age = age + self.style = style + self.sample = sample + self.texture = texture + self.loudness = loudness + self.tempo = tempo + } private enum CodingKeys: String, CodingKey { case id, name, language, languageCode, voiceEngine, isCloned @@ -72,3 +106,24 @@ extension PlayHT { case flac = "flac" } } + +// MARK: - Conformances + +extension PlayHT.Voice: AbstractVoiceConvertible { + public func __conversion() throws -> AbstractVoice { + return AbstractVoice( + voiceID: self.id.rawValue, + name: self.name, + description: nil + ) + } +} + +extension PlayHT.Voice: AbstractVoiceInitiable { + public init(voice: AbstractVoice) throws { + self.init( + id: .init(rawValue: voice.id.rawValue), + name: voice.name + ) + } +} diff --git a/Sources/Rime/Intramodular/Models/Rime.Voice.swift b/Sources/Rime/Intramodular/Models/Rime.Voice.swift index 1a341b41..459a19ba 100644 --- a/Sources/Rime/Intramodular/Models/Rime.Voice.swift +++ b/Sources/Rime/Intramodular/Models/Rime.Voice.swift @@ -6,10 +6,32 @@ // import Foundation +import CorePersistence import Swallow +import LargeLanguageModels extension Rime { public struct Voice: Hashable { + public typealias ID = _TypeAssociatedID + + public init( + name: String, + age: String?, + country: String?, + region: String?, + demographic: String?, + genre: [String]? + ) { + self.id = .init(rawValue: UUID().uuidString) + self.name = name + self.age = age + self.country = country + self.region = region + self.demographic = demographic + self.genre = genre + } + + public let id: ID public let name: String public let age: String? public let country: String? @@ -42,5 +64,30 @@ extension Rime.Voice: Codable { self.region = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.region) self.demographic = try container.decodeIfPresent(String.self, forKey: Rime.Voice.CodingKeys.demographic) self.genre = try container.decodeIfPresent([String].self, forKey: Rime.Voice.CodingKeys.genre) + + self.id = .init(rawValue: UUID().uuidString) + } +} + +extension Rime.Voice: AbstractVoiceInitiable { + public init(voice: AbstractVoice) throws { + self.init( + name: voice.name, + age: nil, + country: nil, + region: nil, + demographic: nil, + genre: nil + ) + } +} + +extension Rime.Voice: AbstractVoiceConvertible { + public func __conversion() throws -> AbstractVoice { + return AbstractVoice( + voiceID: self.id.rawValue, + name: self.name, + description: nil + ) } } From 114db0bd655f650447ad7841aabe89d9aa2229a7 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Tue, 14 Jan 2025 17:08:08 -0700 Subject: [PATCH 62/73] Moved protocol conformances --- ...lient+SpeechSynthesisRequestHandling.swift | 44 +++++++++++ .../AbstractVoice.swift | 0 .../AbstractVoiceSettings.swift | 0 .../SpeechSynthesisRequestHandling.swift | 0 .../VideoGenerationRequestHandling.swift | 1 - .../VideoGenerationSettings.FrameRate.swift | 0 ...deoGenerationSettings.MotionSettings.swift | 0 .../VideoGenerationSettings.Quality.swift | 0 .../VideoGenerationSettings.Resolution.swift | 0 ...ideoGenerationSettings.StyleStrength.swift | 0 .../VideoGenerationSettings.swift | 0 .../VideoModel.swift | 0 .../Intramodular/Models/NeetsAI.Voice.swift | 22 ++++++ ...lient+SpeechSynthesisRequestHandling.swift | 41 ++++++++++ ...lient+SpeechSynthesisRequestHandling.swift | 56 +++++++++++++ .../PlayHT/Intramodular/PlayHT.Client.swift | 6 +- Sources/PlayHT/Intramodular/URL++.swift | 79 +++++++++++++++++++ ...lient+SpeechSynthesisRequestHandling.swift | 55 +++++++++++++ 18 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 Sources/HumeAI/Intramodular/HumeAI.Client+SpeechSynthesisRequestHandling.swift rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else => AbstractVoice (WIP)}/AbstractVoice.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else => AbstractVoice (WIP)}/AbstractVoiceSettings.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else => AbstractVoice (WIP)}/SpeechSynthesisRequestHandling.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else => VideoGeneration (WIP)}/VideoGenerationRequestHandling.swift (98%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.FrameRate.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.MotionSettings.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.Quality.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.Resolution.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.StyleStrength.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else/Video Generation Setttings => VideoGeneration (WIP)}/VideoGenerationSettings.swift (100%) rename Sources/LargeLanguageModels/Intramodular/{WIP - Move Somewhere Else => VideoGeneration (WIP)}/VideoModel.swift (100%) create mode 100644 Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift create mode 100644 Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift create mode 100644 Sources/PlayHT/Intramodular/URL++.swift create mode 100644 Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift diff --git a/Sources/HumeAI/Intramodular/HumeAI.Client+SpeechSynthesisRequestHandling.swift b/Sources/HumeAI/Intramodular/HumeAI.Client+SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..cd5a4e8f --- /dev/null +++ b/Sources/HumeAI/Intramodular/HumeAI.Client+SpeechSynthesisRequestHandling.swift @@ -0,0 +1,44 @@ +// +// HumeAI+ElevenLabsClientProtocol.swift +// Voice +// +// Created by Jared Davidson on 11/22/24. +// + +import Foundation +import SwiftUI +import AVFoundation +import LargeLanguageModels + +extension HumeAI.Client: SpeechSynthesisRequestHandling { + public func availableVoices() async throws -> [AbstractVoice] { + return try await getAllAvailableVoices().map( + { voice in + return AbstractVoice( + voiceID: voice.id, + name: voice.name, + description: nil + ) + }) + } + + public func speech(for text: String, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + throw HumeAI.APIError.unknown(message: "Text to speech not supported") + } + + public func speechToSpeech(inputAudioURL: URL, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + throw HumeAI.APIError.unknown(message: "Speech to speech not supported") + } + + public func upload(voiceWithName name: String, description: String, fileURL: URL) async throws -> AbstractVoice.ID { + throw HumeAI.APIError.unknown(message: "Voice creation is not supported") + } + + public func edit(voice: AbstractVoice.ID, name: String, description: String, fileURL: URL?) async throws -> Bool { + throw HumeAI.APIError.unknown(message: "Voice creation is not supported") + } + + public func delete(voice: AbstractVoice.ID) async throws { + throw HumeAI.APIError.unknown(message: "Voice creation is not supported") + } +} diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoice.swift b/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoice.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoice.swift rename to Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoice.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoiceSettings.swift b/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/AbstractVoiceSettings.swift rename to Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/SpeechSynthesisRequestHandling.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift rename to Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/SpeechSynthesisRequestHandling.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationRequestHandling.swift similarity index 98% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationRequestHandling.swift index d9f9ab1c..bc82693e 100644 --- a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoGenerationRequestHandling.swift +++ b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationRequestHandling.swift @@ -5,7 +5,6 @@ import AVFoundation import Foundation import SwiftUI -import LargeLanguageModels public protocol VideoGenerationRequestHandling { func availableModels() async throws -> [VideoModel] diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.FrameRate.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.FrameRate.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.FrameRate.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.MotionSettings.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.MotionSettings.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.MotionSettings.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.Quality.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Quality.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.Quality.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.Resolution.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.Resolution.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.Resolution.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.StyleStrength.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.StyleStrength.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.StyleStrength.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/Video Generation Setttings/VideoGenerationSettings.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoGenerationSettings.swift diff --git a/Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoModel.swift b/Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoModel.swift similarity index 100% rename from Sources/LargeLanguageModels/Intramodular/WIP - Move Somewhere Else/VideoModel.swift rename to Sources/LargeLanguageModels/Intramodular/VideoGeneration (WIP)/VideoModel.swift diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift index 2f035154..4422720b 100644 --- a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift @@ -6,6 +6,7 @@ // import Foundation +import LargeLanguageModels extension NeetsAI { public struct Voice: Codable, Hashable { @@ -15,3 +16,24 @@ extension NeetsAI { public let supportedModels: [String] } } + +extension NeetsAI.Voice: AbstractVoiceConvertible { + public func __conversion() throws -> AbstractVoice { + return AbstractVoice( + voiceID: self.id, + name: self.title ?? "", + description: self.aliasOf + ) + } +} + +extension NeetsAI.Voice: AbstractVoiceInitiable { + public init(voice: AbstractVoice) throws { + self.init( + id: voice.voiceID, + title: voice.name, + aliasOf: voice.description, + supportedModels: [] + ) + } +} diff --git a/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift b/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..3fa5844b --- /dev/null +++ b/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift @@ -0,0 +1,41 @@ +// +// NeetsAI.Client+SpeechSynthesisRequestHandling.swift +// Voice +// + +import Foundation +import SwiftUI +import AVFoundation +import LargeLanguageModels + +extension NeetsAI.Client: SpeechSynthesisRequestHandling { + public func availableVoices() async throws -> [AbstractVoice] { + return try await getAllAvailableVoices().map( { try $0.__conversion() } ) + } + + public func speech(for text: String, voiceID: String, voiceSettings: LargeLanguageModels.AbstractVoiceSettings, model: String) async throws -> Data { + let audio = try await generateSpeech( + text: text, + voiceId: voiceID, + model: .init(rawValue: model) ?? .mistralai + ) + return audio + } + + public func speechToSpeech(inputAudioURL: URL, voiceID: String, voiceSettings: LargeLanguageModels.AbstractVoiceSettings, model: String) async throws -> Data { + throw NeetsAI.APIError.unknown(message: "Speech to speech not supported") + + } + + public func upload(voiceWithName name: String, description: String, fileURL: URL) async throws -> LargeLanguageModels.AbstractVoice.ID { + throw NeetsAI.APIError.unknown(message: "Uploading Voice is not supported") + } + + public func edit(voice: LargeLanguageModels.AbstractVoice.ID, name: String, description: String, fileURL: URL?) async throws -> Bool { + throw NeetsAI.APIError.unknown(message: "Editing Voice is not supported") + } + + public func delete(voice: LargeLanguageModels.AbstractVoice.ID) async throws { + throw NeetsAI.APIError.unknown(message: "Deleting Voice is not supported") + } +} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift b/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..c987b479 --- /dev/null +++ b/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift @@ -0,0 +1,56 @@ +// +// PlayHT+SpeechSynthesisRequestHandling.swift +// Voice +// +// Created by Jared Davidson on 11/20/24. +// + +import Foundation +import AI +import ElevenLabs +import SwiftUI +import AVFoundation +import LargeLanguageModels + +extension PlayHT.Client: SpeechSynthesisRequestHandling { + public func availableVoices() async throws -> [AbstractVoice] { + let voices: [AbstractVoice] = try await getAllAvailableVoices().map { try $0.__conversion() } + return voices + } + + public func speech(for text: String, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + let data: Data = try await streamTextToSpeech( + text: text, + voice: voiceID, + settings: .init(), + model: .playHT2Turbo + ) + + return data + } + + public func speechToSpeech(inputAudioURL: URL, voiceID: String, voiceSettings: LargeLanguageModels.AbstractVoiceSettings, model: String) async throws -> Data { + throw PlayHT.APIError.unknown(message: "Speech to speech not supported") + } + + public func upload(voiceWithName name: String, description: String, fileURL: URL) async throws -> AbstractVoice.ID { + let mp4URL = try await fileURL.convertAudioToMP4() + let fileURLString = mp4URL.absoluteString + let voiceID = try await instantCloneVoice( + sampleFileURL: fileURLString, + name: name + ) + + try? FileManager.default.removeItem(at: mp4URL) + + return .init(rawValue: voiceID.rawValue) + } + + public func edit(voice: LargeLanguageModels.AbstractVoice.ID, name: String, description: String, fileURL: URL?) async throws -> Bool { + throw PlayHT.APIError.unknown(message: "Voice editing not supported") + } + + public func delete(voice: LargeLanguageModels.AbstractVoice.ID) async throws { + try await deleteClonedVoice(voice: .init(rawValue: voice.rawValue)) + } +} diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client.swift b/Sources/PlayHT/Intramodular/PlayHT.Client.swift index 66e6e80f..eb63bfa8 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client.swift @@ -59,14 +59,14 @@ extension PlayHT.Client: CoreMI._ServiceClientProtocol { extension PlayHT.Client { public func getAllAvailableVoices() async throws -> [PlayHT.Voice] { - async let htVoices = availableVoices() - async let clonedVoices = clonedVoices() + async let htVoices = self.getAvailableVoices() + async let clonedVoices = self.clonedVoices() let (available, cloned) = try await (htVoices, clonedVoices) return available + cloned } - public func availableVoices() async throws -> [PlayHT.Voice] { + public func getAvailableVoices() async throws -> [PlayHT.Voice] { try await run(\.listVoices).voices } diff --git a/Sources/PlayHT/Intramodular/URL++.swift b/Sources/PlayHT/Intramodular/URL++.swift new file mode 100644 index 00000000..f584da1f --- /dev/null +++ b/Sources/PlayHT/Intramodular/URL++.swift @@ -0,0 +1,79 @@ +// +// URL++.swift +// AI +// +// Created by Jared Davidson on 1/14/25. +// + +import AVFoundation +import AudioToolbox + +// FIXME: - This needs to be moved somewhere else (@archetapp) + +extension URL { + func convertAudioToMP4() async throws -> URL { + let outputURL = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("mp4") + + let asset = AVURLAsset(url: self) + + let composition = AVMutableComposition() + guard let compositionTrack = composition.addMutableTrack( + withMediaType: .audio, + preferredTrackID: kCMPersistentTrackID_Invalid + ) else { + throw NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not create composition track"]) + } + + guard let audioTrack = try await asset.loadTracks(withMediaType: .audio).first else { + throw NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "No audio track found"]) + } + + let timeRange = CMTimeRange(start: .zero, duration: try await asset.load(.duration)) + for i in 0..<4 { + try compositionTrack.insertTimeRange( + timeRange, + of: audioTrack, + at: CMTime(seconds: Double(i) * timeRange.duration.seconds, preferredTimescale: 600) + ) + } + + guard let exportSession = AVAssetExportSession( + asset: composition, + presetName: AVAssetExportPresetPassthrough + ) else { + throw NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not create export session"]) + } + + exportSession.outputURL = outputURL + exportSession.outputFileType = AVFileType.mp4 + exportSession.shouldOptimizeForNetworkUse = true + + // Create a tuple of values we need to check after export + try await withCheckedThrowingContinuation { continuation in + let mainQueue = DispatchQueue.main + exportSession.exportAsynchronously { + mainQueue.async { + switch exportSession.status { + case .completed: + continuation.resume() + case .failed: + continuation.resume(throwing: exportSession.error ?? NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export failed"])) + case .cancelled: + continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"])) + default: + continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown export error"])) + } + } + } + } + + let fileSize = try FileManager.default.attributesOfItem(atPath: outputURL.path)[.size] as? Int ?? 0 + if fileSize < 5000 { // 5KB minimum + throw NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Converted file too small"]) + } + + return outputURL + } +} diff --git a/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift b/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift new file mode 100644 index 00000000..93126293 --- /dev/null +++ b/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift @@ -0,0 +1,55 @@ +// +// Rime+SpeechSynthesisRequestHandling.swift +// Voice +// +// Created by Jared Davidson on 11/21/24. +// + +import Foundation +import AI +import ElevenLabs +import SwiftUI +import AVFoundation + +extension Rime.Client: SpeechSynthesisRequestHandling { + public func availableVoices() async throws -> [AbstractVoice] { + return try await getAllAvailableVoiceDetails().map { try $0.__conversion() } + } + + public func speech(for text: String, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + return try await streamTextToSpeech( + text: text, + voice: voiceID, + outputAudio: .MP3, + model: .mist + ) + } + + public func speechToSpeech(inputAudioURL: URL, voiceID: String, voiceSettings: AbstractVoiceSettings, model: String) async throws -> Data { + throw Rime.APIError.unknown(message: "Speech to speech not supported") + } + + public func upload(voiceWithName name: String, description: String, fileURL: URL) async throws -> AbstractVoice.ID { + throw Rime.APIError.unknown(message: "Voice creation is not supported") + } + + public func edit(voice: AbstractVoice.ID, name: String, description: String, fileURL: URL?) async throws -> Bool { + throw Rime.APIError.unknown(message: "Voice creation is not supported") + } + + public func delete(voice: AbstractVoice.ID) async throws { + throw Rime.APIError.unknown(message: "Voice creation is not supported") + } + + public func availableVoices() async throws -> [ElevenLabs.Voice] { + return try await getAllAvailableVoiceDetails().map { voice in + ElevenLabs.Voice( + voiceID: voice.name, + name: voice.name, + description: voice.demographic, + isOwner: false + ) + } + } + +} From f2f807c282e67ee6cb4cac35f3e471d0df0011f7 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Thu, 16 Jan 2025 18:18:42 +0530 Subject: [PATCH 63/73] Cleanup --- .../SpeechSynthesisRequestHandling.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift index 1de670e4..e7d762d0 100644 --- a/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift +++ b/Sources/AI/WIP - Move Somewhere Else/SpeechSynthesisRequestHandling.swift @@ -6,7 +6,6 @@ // import Foundation -import AI import ElevenLabs import SwiftUI From 3aba2ada446184b215d872de01cec1abc7a88a6c Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Fri, 17 Jan 2025 00:43:00 +0530 Subject: [PATCH 64/73] Update Gemini (Incomplete) --- ...emini.APISpecification.RequestBodies.swift | 38 +++++++++---- .../API/_Gemini.APISpecification.swift | 53 +++++++++++++++++-- .../Intramodular/_Gemini.Client+Files.swift | 46 +++++++++------- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.RequestBodies.swift b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.RequestBodies.swift index 7003aebd..0eaaa91f 100644 --- a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.RequestBodies.swift +++ b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.RequestBodies.swift @@ -142,10 +142,31 @@ extension _Gemini.APISpecification { } } - public struct FileUploadInput: Codable, HTTPRequest.Multipart.ContentConvertible { + public struct FinalizeFileUploadInput { + public let data: Data + public let uploadUrl: String + public let fileSize: Int + + public init(data: Data, uploadUrl: String, fileSize: Int) { + self.data = data + self.uploadUrl = uploadUrl + self.fileSize = fileSize + } + } + + public struct StartFileUploadInput: Codable { + public struct UploadMetadata: Codable { + let file: FileMetadata + + struct FileMetadata: Codable { + let display_name: String + } + } + public let fileData: Data public let mimeType: String public let displayName: String + public let metadata: UploadMetadata public init( fileData: Data, @@ -155,11 +176,12 @@ extension _Gemini.APISpecification { self.fileData = fileData self.mimeType = mimeType self.displayName = displayName + self.metadata = .init(file: .init(display_name: displayName)) } - + /* public func __conversion() throws -> HTTPRequest.Multipart.Content { var result = HTTPRequest.Multipart.Content() - + // TODO: - Add this to `HTTPMediaType` @jared @vmanot let fileExtension: String = { guard let subtype = mimeType.split(separator: "/").last else { @@ -188,17 +210,11 @@ extension _Gemini.APISpecification { } }() - result.append( - .file( - named: "file", - data: fileData, - filename: "\(displayName).\(fileExtension)", - contentType: .init(rawValue: mimeType) - ) - ) + result.ap return result } + */ } public struct DeleteFileInput: Codable { diff --git a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift index fe289d07..4474386e 100644 --- a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift +++ b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift @@ -75,11 +75,29 @@ extension _Gemini { // Initial Upload Request endpoint @POST @Path("/upload/v1beta/files") - @Header([ - "X-Goog-Upload-Command": "start, upload, finalize" - ]) - @Body(multipart: .input) - var uploadFile = Endpoint() + @Header({ context in + [ + HTTPHeaderField(key: "X-Goog-Upload-Protocol", value: "resumable"), + HTTPHeaderField(key: "X-Goog-Upload-Command", value: "start"), + HTTPHeaderField(key: "X-Goog-Upload-Header-Content-Length", value: "\(context.input.fileData.count)"), + HTTPHeaderField(key: "X-Goog-Upload-Header-Content-Type", value: context.input.mimeType), + HTTPHeaderField.contentType(.json) + ] + }) + @Body(json: \RequestBodies.StartFileUploadInput.metadata) + var startFileUpload = Endpoint() + + @POST + @Path({ context in context.input.uploadUrl }) + @Header({ context in + [ + HTTPHeaderField(key: "Content-Length", value: "\(context.input.fileSize)"), + HTTPHeaderField(key: "X-Goog-Upload-Offset", value: "0"), + HTTPHeaderField(key: "X-Goog-Upload-Command", value: "upload, finalize") + ] + }) + @Body(json: \RequestBodies.FinalizeFileUploadInput.data) + var finalizeFileUpload = Endpoint() // File Status endpoint @GET @@ -157,6 +175,7 @@ extension _Gemini.APISpecification { context: context ) + // FIXME: (@jared) - why are you replacing the query instead of appending a new query item? is this intentional? if let apiKey = context.root.configuration.apiKey { request = request.query([.init(name: "key", value: apiKey)]) } @@ -173,10 +192,34 @@ extension _Gemini.APISpecification { try response.validate() + + if let options: _Gemini.APISpecification.Options = context.options as? _Gemini.APISpecification.Options, let headerKey = options.outputHeaderKey { + print("HEADERS: \(response.headerFields)") + let stringValue: String? = response.headerFields.first (where: { $0.key == headerKey })?.value + print(stringValue) + + switch Output.self { + case String.self: + return (try stringValue.unwrap()) as! Output + case Optional.self: + return stringValue as! Output + default: + throw _Gemini.APIError.invalidContentType + } + } + return try response.decode( Output.self, keyDecodingStrategy: .convertFromSnakeCase ) } } + + public class Options { + var outputHeaderKey: HTTPHeaderField.Key? + + init(outputHeaderKey: HTTPHeaderField.Key? = nil) { + self.outputHeaderKey = outputHeaderKey + } + } } diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift index b6fa298c..7ff3c88a 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift @@ -9,6 +9,17 @@ import Merge import NetworkKit import Swallow +fileprivate enum TempError: CustomStringError, Error { + case fetchedResponse + + public var description: String { + switch self { + case .fetchedResponse: + return "Got response url from header" + } + } +} + extension _Gemini.Client { public func uploadFile( from data: Data, @@ -20,27 +31,26 @@ extension _Gemini.Client { throw FileProcessingError.invalidFileName } - do { - var mimeType: String? = mimeType?.rawValue ?? _MediaAssetFileType(data)?.mimeType - - if mimeType == nil, let swiftType { - mimeType = HTTPMediaType(_swiftType: swiftType)?.rawValue - } - - let input = _Gemini.APISpecification.RequestBodies.FileUploadInput( - fileData: data, - mimeType: try mimeType.unwrap(), - displayName: displayName - ) - - let response = try await run(\.uploadFile, with: input) - - return response.file - } catch { - throw _Gemini.APIError.unknown(message: "File upload failed: \(error.localizedDescription)") + var mimeType: String? = mimeType?.rawValue ?? _MediaAssetFileType(data)?.mimeType + + if mimeType == nil, let swiftType { + mimeType = HTTPMediaType(_swiftType: swiftType)?.rawValue } + + let input = _Gemini.APISpecification.RequestBodies.StartFileUploadInput( + fileData: data, + mimeType: try mimeType.unwrap(), + displayName: displayName + ) + + let uploadURLString: String = try await run(\.startFileUpload, with: input, options: _Gemini.APISpecification.Options(outputHeaderKey: .custom("x-goog-upload-url"))).value + + let result: _Gemini.APISpecification.ResponseBodies.FileUpload = try await run(\.finalizeFileUpload, with: _Gemini.APISpecification.RequestBodies.FinalizeFileUploadInput(data: data, uploadUrl: uploadURLString, fileSize: data.count)) + + return result.file } + public func uploadFile( from url: URL, mimeType: HTTPMediaType?, From 5b3e1361cd40a0f87369c5605bf88f94dc189c54 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Fri, 17 Jan 2025 01:21:54 +0530 Subject: [PATCH 65/73] Fix --- .../API/_Gemini.APISpecification.swift | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift index 4474386e..cecb0399 100644 --- a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift +++ b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift @@ -70,11 +70,13 @@ extension _Gemini { "/v1beta/models/\(context.input.model):generateContent" }) @Body(json: \.requestBody) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var generateContent = Endpoint() // Initial Upload Request endpoint @POST @Path("/upload/v1beta/files") + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) @Header({ context in [ HTTPHeaderField(key: "X-Goog-Upload-Protocol", value: "resumable"), @@ -88,7 +90,7 @@ extension _Gemini { var startFileUpload = Endpoint() @POST - @Path({ context in context.input.uploadUrl }) + @AbsolutePath({ $0.input.uploadUrl }) @Header({ context in [ HTTPHeaderField(key: "Content-Length", value: "\(context.input.fileSize)"), @@ -96,7 +98,7 @@ extension _Gemini { HTTPHeaderField(key: "X-Goog-Upload-Command", value: "upload, finalize") ] }) - @Body(json: \RequestBodies.FinalizeFileUploadInput.data) + @Body(data: \RequestBodies.FinalizeFileUploadInput.data) var finalizeFileUpload = Endpoint() // File Status endpoint @@ -104,6 +106,7 @@ extension _Gemini { @Path({ context -> String in "/v1beta/\(context.input.name.rawValue)" }) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var getFile = Endpoint() @GET @@ -119,8 +122,13 @@ extension _Gemini { parameters["pageToken"] = pageToken } + if let apiKey = context.root.configuration.apiKey { + parameters["key"] = apiKey + } + return parameters }) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var listFiles = Endpoint() // Delete File endpoint @@ -128,24 +136,28 @@ extension _Gemini { @Path({ context -> String in "/\(context.input.fileURL.path)" }) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var deleteFile = Endpoint() - //Fine Tuning + // Fine Tuning @POST @Path("/v1beta/tunedModels") @Body(json: \.requestBody) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var createTunedModel = Endpoint() @GET @Path({ context -> String in "/v1/\(context.input.operationName)" }) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var getTuningOperation = Endpoint() @GET @Path({ context -> String in "/v1beta/\(context.input.modelName)" }) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var getTunedModel = Endpoint() @POST @@ -153,6 +165,7 @@ extension _Gemini { "/v1beta/\(context.input.model):generateContent" // Use the model name directly }) @Body(json: \.requestBody) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var generateTunedContent = Endpoint() @POST @@ -160,6 +173,7 @@ extension _Gemini { "/v1beta/models/\(context.input.model):embedContent" }) @Body(json: \.input) + @Query({ $0.root.configuration.apiKey.map { ["key": $0] } ?? [:] }) var generateEmbedding = Endpoint() } } @@ -175,10 +189,7 @@ extension _Gemini.APISpecification { context: context ) - // FIXME: (@jared) - why are you replacing the query instead of appending a new query item? is this intentional? - if let apiKey = context.root.configuration.apiKey { - request = request.query([.init(name: "key", value: apiKey)]) - } + print("REQUEST URL: \(request.url)") return request } From 1f990563cffcea707608a3a6687deb6487c2a753 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Fri, 17 Jan 2025 09:26:45 -0700 Subject: [PATCH 66/73] Fixed build issue --- Package.swift | 1 + Sources/AI/AnySpeechSynthesisRequestHandling.swift | 3 +++ .../Intramodular/Models/NeetsAI.Voice.swift | 2 +- ...sAI.Client+SpeechSynthesisRequestHandling.swift | 14 +++++++++++++- Sources/NeetsAI/module.swift | 2 ++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index f731cfd7..df47734a 100644 --- a/Package.swift +++ b/Package.swift @@ -389,6 +389,7 @@ let package = Package( "Ollama", "OpenAI", "Swallow", + "NeetsAI", ], path: "Sources/AI", swiftSettings: [ diff --git a/Sources/AI/AnySpeechSynthesisRequestHandling.swift b/Sources/AI/AnySpeechSynthesisRequestHandling.swift index 18dce27e..5684958f 100644 --- a/Sources/AI/AnySpeechSynthesisRequestHandling.swift +++ b/Sources/AI/AnySpeechSynthesisRequestHandling.swift @@ -7,6 +7,7 @@ import ElevenLabs import LargeLanguageModels +import NeetsAI public struct AnySpeechSynthesisRequestHandling: Hashable { private let _hashValue: Int @@ -17,6 +18,8 @@ public struct AnySpeechSynthesisRequestHandling: Hashable { switch base { case is ElevenLabs.Client: return "ElevenLabs" + case is NeetsAI.Client: + return "NeetsAI" default: fatalError() } diff --git a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift index 4422720b..ee23943a 100644 --- a/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift +++ b/Sources/NeetsAI/Intramodular/Models/NeetsAI.Voice.swift @@ -30,7 +30,7 @@ extension NeetsAI.Voice: AbstractVoiceConvertible { extension NeetsAI.Voice: AbstractVoiceInitiable { public init(voice: AbstractVoice) throws { self.init( - id: voice.voiceID, + id: .init(voice.voiceID), title: voice.name, aliasOf: voice.description, supportedModels: [] diff --git a/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift b/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift index 3fa5844b..973024bd 100644 --- a/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift +++ b/Sources/NeetsAI/Intramodular/NeetsAI.Client+SpeechSynthesisRequestHandling.swift @@ -10,7 +10,11 @@ import LargeLanguageModels extension NeetsAI.Client: SpeechSynthesisRequestHandling { public func availableVoices() async throws -> [AbstractVoice] { - return try await getAllAvailableVoices().map( { try $0.__conversion() } ) + let voices = try await getAllAvailableVoices() + .map({ try $0.__conversion() }) + .filter({ !$0.name.isEmpty }) + .unique(by: \.name) + return voices } public func speech(for text: String, voiceID: String, voiceSettings: LargeLanguageModels.AbstractVoiceSettings, model: String) async throws -> Data { @@ -39,3 +43,11 @@ extension NeetsAI.Client: SpeechSynthesisRequestHandling { throw NeetsAI.APIError.unknown(message: "Deleting Voice is not supported") } } + +// FIXME: - REMOVE ME +extension Sequence { + func unique(by keyPath: KeyPath) -> [Element] { + var seen = Set() + return filter { seen.insert($0[keyPath: keyPath]).inserted } + } +} diff --git a/Sources/NeetsAI/module.swift b/Sources/NeetsAI/module.swift index 1c4d3b99..5b26df46 100644 --- a/Sources/NeetsAI/module.swift +++ b/Sources/NeetsAI/module.swift @@ -5,3 +5,5 @@ // Created by Jared Davidson on 11/22/24. // +@_exported import Swallow +@_exported import SwallowMacrosClient From 3fe5468107870109a8c0a7fabdca7abbf1fa87d9 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Sat, 18 Jan 2025 22:45:30 +0530 Subject: [PATCH 67/73] Update --- Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift index 7ff3c88a..020e3cde 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift @@ -6,6 +6,7 @@ import CoreMI import Dispatch import FoundationX import Merge +import Media import NetworkKit import Swallow @@ -50,6 +51,9 @@ extension _Gemini.Client { return result.file } + public func upload(file: any MediaFile) async throws { + try await self.uploadFile(from: file.url, mimeType: HTTPMediaType(fileURL: file.url), displayName: file.name) + } public func uploadFile( from url: URL, From 6fac42dba41a5b3b8038d1e66e00c28086a57be8 Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Mon, 20 Jan 2025 04:03:54 +0530 Subject: [PATCH 68/73] Cleanup --- Package.swift | 3 ++- .../AbstractVoice (WIP)/AbstractVoiceSettings.swift | 2 +- .../Intramodular/API/_Gemini.APISpecification.swift | 10 +--------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index df47734a..ee06cd68 100644 --- a/Package.swift +++ b/Package.swift @@ -191,7 +191,8 @@ let package = Package( "LargeLanguageModels", "Merge", "NetworkKit", - "Swallow" + "Swallow", + "Media" ], path: "Sources/_Gemini", swiftSettings: [ diff --git a/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift b/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift index 34fd03b6..b54b685f 100644 --- a/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift +++ b/Sources/LargeLanguageModels/Intramodular/AbstractVoice (WIP)/AbstractVoiceSettings.swift @@ -5,7 +5,7 @@ // Created by Jared Davidson on 10/30/24. // -import SwiftUIZ +import SwiftUIX import CorePersistence public struct AbstractVoiceSettings: Codable, Sendable, Initiable, Equatable { diff --git a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift index cecb0399..587217f2 100644 --- a/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift +++ b/Sources/_Gemini/Intramodular/API/_Gemini.APISpecification.swift @@ -184,13 +184,11 @@ extension _Gemini.APISpecification { from input: Input, context: BuildRequestContext ) throws -> Request { - var request = try super.buildRequestBase( + let request = try super.buildRequestBase( from: input, context: context ) - print("REQUEST URL: \(request.url)") - return request } @@ -198,16 +196,10 @@ extension _Gemini.APISpecification { from response: Request.Response, context: DecodeOutputContext ) throws -> Output { - - print(response) - try response.validate() - if let options: _Gemini.APISpecification.Options = context.options as? _Gemini.APISpecification.Options, let headerKey = options.outputHeaderKey { - print("HEADERS: \(response.headerFields)") let stringValue: String? = response.headerFields.first (where: { $0.key == headerKey })?.value - print(stringValue) switch Output.self { case String.self: From 96c7895d1619a34fa7ebbe5570451f7db701690c Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Mon, 20 Jan 2025 23:40:06 +0530 Subject: [PATCH 69/73] Fix package.swift --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index ee06cd68..6dd9893e 100644 --- a/Package.swift +++ b/Package.swift @@ -92,6 +92,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/vmanot/CorePersistence.git", branch: "main"), + .package(url: "https://github.com/vmanot/Media", branch: "main"), .package(url: "https://github.com/vmanot/Merge.git", branch: "master"), .package(url: "https://github.com/vmanot/NetworkKit.git", branch: "master"), .package(url: "https://github.com/vmanot/Swallow.git", branch: "master"), From 6c47824c71022e71eed9e01c28934b76dcda1544 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Sat, 22 Feb 2025 16:33:34 -0800 Subject: [PATCH 70/73] Update package --- Sources/PlayHT/Intramodular/URL++.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Sources/PlayHT/Intramodular/URL++.swift b/Sources/PlayHT/Intramodular/URL++.swift index f584da1f..622b5933 100644 --- a/Sources/PlayHT/Intramodular/URL++.swift +++ b/Sources/PlayHT/Intramodular/URL++.swift @@ -52,18 +52,17 @@ extension URL { // Create a tuple of values we need to check after export try await withCheckedThrowingContinuation { continuation in - let mainQueue = DispatchQueue.main exportSession.exportAsynchronously { - mainQueue.async { + Task { @MainActor in switch exportSession.status { - case .completed: - continuation.resume() - case .failed: - continuation.resume(throwing: exportSession.error ?? NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export failed"])) - case .cancelled: - continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"])) - default: - continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown export error"])) + case .completed: + continuation.resume() + case .failed: + continuation.resume(throwing: exportSession.error ?? NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export failed"])) + case .cancelled: + continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"])) + default: + continuation.resume(throwing: NSError(domain: "AudioConversion", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown export error"])) } } } From 1f2f38132d55439b943ca7593aa22473b6ce27b8 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Thu, 10 Apr 2025 22:06:16 -0700 Subject: [PATCH 71/73] Update package --- ...HT.Client+SpeechSynthesisRequestHandling.swift | 2 -- ...me.Client+SpeechSynthesisRequestHandling.swift | 15 +-------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift b/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift index 8132946d..5ad471cd 100644 --- a/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift +++ b/Sources/PlayHT/Intramodular/PlayHT.Client+SpeechSynthesisRequestHandling.swift @@ -6,8 +6,6 @@ // import Foundation -import AI -import ElevenLabs import SwiftUI import AVFoundation import LargeLanguageModels diff --git a/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift b/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift index 93126293..f4bd5023 100644 --- a/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift +++ b/Sources/Rime/Intramodular/Rime.Client+SpeechSynthesisRequestHandling.swift @@ -5,9 +5,8 @@ // Created by Jared Davidson on 11/21/24. // +import LargeLanguageModels import Foundation -import AI -import ElevenLabs import SwiftUI import AVFoundation @@ -40,16 +39,4 @@ extension Rime.Client: SpeechSynthesisRequestHandling { public func delete(voice: AbstractVoice.ID) async throws { throw Rime.APIError.unknown(message: "Voice creation is not supported") } - - public func availableVoices() async throws -> [ElevenLabs.Voice] { - return try await getAllAvailableVoiceDetails().map { voice in - ElevenLabs.Voice( - voiceID: voice.name, - name: voice.name, - description: voice.demographic, - isOwner: false - ) - } - } - } From b18859e8228f007a42d2d38069bbedff659477f3 Mon Sep 17 00:00:00 2001 From: "Jared Davidson (Archetapp)" Date: Sat, 12 Apr 2025 15:23:12 -0600 Subject: [PATCH 72/73] added at least 1 import, cleanup --- .../AnySpeechSynthesisRequestHandling.swift | 2 + ...nLabs.APISpecification.RequestBodies.swift | 650 +++++++++--------- .../Models/ElevenLabs.DubbingOptions.swift | 2 + .../Intramodular/Models/HumeAI.Chat.swift | 2 + .../Models/HumeAI.ChatEvent.swift | 2 + .../Models/HumeAI.ChatGroup.swift | 2 + .../Models/HumeAI.ChatMessage.swift | 2 + .../Models/HumeAI.ChatResponse.swift | 2 + .../Intramodular/Models/HumeAI.Config.swift | 2 + .../Intramodular/Models/HumeAI.Dataset.swift | 2 + .../Intramodular/Models/HumeAI.File.swift | 2 + .../Models/HumeAI.FileInput.swift | 2 + .../Intramodular/Models/HumeAI.Job.swift | 2 + .../Intramodular/Models/HumeAI.Models.swift | 2 + .../Intramodular/Models/HumeAI.Prompt.swift | 2 + .../Intramodular/Models/HumeAI.Tool.swift | 2 + ...etsAI.APISpecification.RequestBodies.swift | 2 + .../Models/PlayHT.VoiceSettings.swift | 2 + 18 files changed, 357 insertions(+), 327 deletions(-) diff --git a/Sources/AI/AnySpeechSynthesisRequestHandling.swift b/Sources/AI/AnySpeechSynthesisRequestHandling.swift index 5684958f..f6f82af9 100644 --- a/Sources/AI/AnySpeechSynthesisRequestHandling.swift +++ b/Sources/AI/AnySpeechSynthesisRequestHandling.swift @@ -9,6 +9,8 @@ import ElevenLabs import LargeLanguageModels import NeetsAI +// FIXME: - (@archetapp) Is this the best place to put this file? + public struct AnySpeechSynthesisRequestHandling: Hashable { private let _hashValue: Int diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift index b0abe155..1ab5be7b 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift @@ -11,383 +11,379 @@ import Merge extension ElevenLabs.APISpecification { enum RequestBodies { - - } -} - -extension ElevenLabs.APISpecification.RequestBodies { - public struct SpeechRequest: Codable, Hashable, Equatable { - public let text: String - public let languageCode: String? - public let voiceSettings: ElevenLabs.VoiceSettings - public let model: ElevenLabs.Model - - private enum CodingKeys: String, CodingKey { - case text - case voiceSettings = "voice_settings" - case model = "model_id" - case languageCode = "language_code" - } - - public init( - text: String, - languageCode: String?, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) { - self.text = text - self.languageCode = languageCode - self.voiceSettings = voiceSettings - self.model = model - } - } - - public struct TextToSpeechInput: Codable, Hashable { - public let voiceId: String - public let requestBody: SpeechRequest - - public init(voiceId: String, requestBody: SpeechRequest) { - self.voiceId = voiceId - self.requestBody = requestBody + public struct SpeechRequest: Codable, Hashable, Equatable { + public let text: String + public let languageCode: String? + public let voiceSettings: ElevenLabs.VoiceSettings + public let model: ElevenLabs.Model + + private enum CodingKeys: String, CodingKey { + case text + case voiceSettings = "voice_settings" + case model = "model_id" + case languageCode = "language_code" + } + + public init( + text: String, + languageCode: String?, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) { + self.text = text + self.languageCode = languageCode + self.voiceSettings = voiceSettings + self.model = model + } } - } - - public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let audioURL: URL - public let languageCode: String? - public let model: ElevenLabs.Model - public let voiceSettings: ElevenLabs.VoiceSettings - public init( - voiceId: String, - audioURL: URL, - languageCode: String?, - model: ElevenLabs.Model, - voiceSettings: ElevenLabs.VoiceSettings - ) { - self.voiceId = voiceId - self.audioURL = audioURL - self.languageCode = languageCode - self.model = model - self.voiceSettings = voiceSettings + public struct TextToSpeechInput: Codable, Hashable { + public let voiceId: String + public let requestBody: SpeechRequest + + public init(voiceId: String, requestBody: SpeechRequest) { + self.voiceId = voiceId + self.requestBody = requestBody + } } - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "model_id", - value: model.rawValue - ) - ) + public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let audioURL: URL + public let languageCode: String? + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings - if let languageCode { - result.append( - .text( - named: "language_code", - value: languageCode - ) - ) + public init( + voiceId: String, + audioURL: URL, + languageCode: String?, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.languageCode = languageCode + self.model = model + self.voiceSettings = voiceSettings } - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - if let voiceSettingsData = try? encoder.encode(voiceSettings), - let voiceSettingsString = String( - data: voiceSettingsData, - encoding: .utf8 - ) { + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + result.append( .text( - named: "voice_settings", - value: voiceSettingsString + named: "model_id", + value: model.rawValue ) ) - } - - if let fileData = try? Data(contentsOf: audioURL) { - result.append( - .file( - named: "audio", - data: fileData, - filename: audioURL.lastPathComponent, - contentType: .mpeg + + if let languageCode { + result.append( + .text( + named: "language_code", + value: languageCode + ) ) - ) + } + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { + result.append( + .text( + named: "voice_settings", + value: voiceSettingsString + ) + ) + } + + if let fileData = try? Data(contentsOf: audioURL) { + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg + ) + ) + } + + return result } - - return result - } - } - - public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let name: String - public let description: String - public let fileURL: URL - - public init( - name: String, - description: String, - fileURL: URL - ) { - self.name = name - self.description = description - self.fileURL = fileURL } - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() + public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let name: String + public let description: String + public let fileURL: URL - result.append( - .text( - named: "name", - value: name - ) - ) - - result.append( - .text( - named: "description", - value: description - ) - ) + public init( + name: String, + description: String, + fileURL: URL + ) { + self.name = name + self.description = description + self.fileURL = fileURL + } - if let fileData = try? Data(contentsOf: fileURL) { + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .m4a + .text( + named: "name", + value: name ) ) - } - - return result - } - } - - public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let name: String - public let description: String? - public let fileURL: URL? - - public init( - voiceId: String, - name: String, - description: String? = nil, - fileURL: URL? = nil - ) { - self.voiceId = voiceId - self.name = name - self.description = description - self.fileURL = fileURL - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "name", - value: name - ) - ) - - if let description = description { + result.append( .text( named: "description", value: description ) ) - } - - if let fileURL = fileURL, - let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .m4a + + if let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .m4a + ) ) - ) + } + + return result } - - return result } - } - - public struct DubbingRequest: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { - public let name: String? - public let sourceURL: URL? - public let sourceLang: String? - public let targetLang: String - public let numSpeakers: Int? - public let watermark: Bool? - public let startTime: Int? - public let endTime: Int? - public let highestResolution: Bool? - public let dropBackgroundAudio: Bool? - public let useProfanityFilter: Bool? - public let fileData: Data? - public init( - name: String? = nil, - sourceURL: URL? = nil, - sourceLang: String? = nil, - targetLang: String, - numSpeakers: Int? = nil, - watermark: Bool? = nil, - startTime: Int? = nil, - endTime: Int? = nil, - highestResolution: Bool? = nil, - dropBackgroundAudio: Bool? = nil, - useProfanityFilter: Bool? = nil, - fileData: Data? = nil - ) { - self.name = name - self.sourceURL = sourceURL - self.sourceLang = sourceLang - self.targetLang = targetLang - self.numSpeakers = numSpeakers - self.watermark = watermark - self.startTime = startTime - self.endTime = endTime - self.highestResolution = highestResolution - self.dropBackgroundAudio = dropBackgroundAudio - self.useProfanityFilter = useProfanityFilter - self.fileData = fileData - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - if let name { - result.append(.text(named: "name", value: name)) - } - - if let sourceURL { - result.append(.text(named: "source_url", value: sourceURL.absoluteString)) - } - - if let sourceLang { - result.append(.text(named: "source_lang", value: sourceLang)) - } - - result.append(.text(named: "target_lang", value: targetLang)) - - if let numSpeakers { - result.append(.text(named: "num_speakers", value: String(numSpeakers))) - } - - if let watermark { - result.append(.text(named: "watermark", value: String(watermark))) - } - - if let startTime { - result.append(.text(named: "start_time", value: String(startTime))) - } - - if let endTime { - result.append(.text(named: "end_time", value: String(endTime))) - } - - if let highestResolution { - result.append(.text(named: "highest_resolution", value: String(highestResolution))) - } + public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let name: String + public let description: String? + public let fileURL: URL? - if let dropBackgroundAudio { - result.append(.text(named: "drop_background_audio", value: String(dropBackgroundAudio))) + public init( + voiceId: String, + name: String, + description: String? = nil, + fileURL: URL? = nil + ) { + self.voiceId = voiceId + self.name = name + self.description = description + self.fileURL = fileURL } - if let useProfanityFilter { - result.append(.text(named: "use_profanity_filter", value: String(useProfanityFilter))) - } - - if let fileData { + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + result.append( - .file( - named: "file", - data: fileData, - filename: "input.mp4", - contentType: .mp4 + .text( + named: "name", + value: name ) ) + + if let description = description { + result.append( + .text( + named: "description", + value: description + ) + ) + } + + if let fileURL = fileURL, + let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .m4a + ) + ) + } + + return result } - - return result } - } - public struct DubbingInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { - public let voiceId: String - public let audioURL: URL - public let languageCode: String - public let model: ElevenLabs.Model - public let voiceSettings: ElevenLabs.VoiceSettings - public init( - voiceId: String, - audioURL: URL, - languageCode: String, - model: ElevenLabs.Model, - voiceSettings: ElevenLabs.VoiceSettings - ) { - self.voiceId = voiceId - self.audioURL = audioURL - self.languageCode = languageCode - self.model = model - self.voiceSettings = voiceSettings - } - - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() + public struct DubbingRequest: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let name: String? + public let sourceURL: URL? + public let sourceLang: String? + public let targetLang: String + public let numSpeakers: Int? + public let watermark: Bool? + public let startTime: Int? + public let endTime: Int? + public let highestResolution: Bool? + public let dropBackgroundAudio: Bool? + public let useProfanityFilter: Bool? + public let fileData: Data? - result.append( - .text( - named: "model_id", - value: model.rawValue - ) - ) + public init( + name: String? = nil, + sourceURL: URL? = nil, + sourceLang: String? = nil, + targetLang: String, + numSpeakers: Int? = nil, + watermark: Bool? = nil, + startTime: Int? = nil, + endTime: Int? = nil, + highestResolution: Bool? = nil, + dropBackgroundAudio: Bool? = nil, + useProfanityFilter: Bool? = nil, + fileData: Data? = nil + ) { + self.name = name + self.sourceURL = sourceURL + self.sourceLang = sourceLang + self.targetLang = targetLang + self.numSpeakers = numSpeakers + self.watermark = watermark + self.startTime = startTime + self.endTime = endTime + self.highestResolution = highestResolution + self.dropBackgroundAudio = dropBackgroundAudio + self.useProfanityFilter = useProfanityFilter + self.fileData = fileData + } - result.append( - .text( - named: "language_code", - value: languageCode - ) - ) + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + + if let name { + result.append(.text(named: "name", value: name)) + } + + if let sourceURL { + result.append(.text(named: "source_url", value: sourceURL.absoluteString)) + } + + if let sourceLang { + result.append(.text(named: "source_lang", value: sourceLang)) + } + + result.append(.text(named: "target_lang", value: targetLang)) + + if let numSpeakers { + result.append(.text(named: "num_speakers", value: String(numSpeakers))) + } + + if let watermark { + result.append(.text(named: "watermark", value: String(watermark))) + } + + if let startTime { + result.append(.text(named: "start_time", value: String(startTime))) + } + + if let endTime { + result.append(.text(named: "end_time", value: String(endTime))) + } + + if let highestResolution { + result.append(.text(named: "highest_resolution", value: String(highestResolution))) + } + + if let dropBackgroundAudio { + result.append(.text(named: "drop_background_audio", value: String(dropBackgroundAudio))) + } + + if let useProfanityFilter { + result.append(.text(named: "use_profanity_filter", value: String(useProfanityFilter))) + } + + if let fileData { + result.append( + .file( + named: "file", + data: fileData, + filename: "input.mp4", + contentType: .mp4 + ) + ) + } + + return result + } + } + public struct DubbingInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible { + public let voiceId: String + public let audioURL: URL + public let languageCode: String + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings + + public init( + voiceId: String, + audioURL: URL, + languageCode: String, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.languageCode = languageCode + self.model = model + self.voiceSettings = voiceSettings + } - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - if let voiceSettingsData = try? encoder.encode(voiceSettings), - let voiceSettingsString = String( - data: voiceSettingsData, - encoding: .utf8 - ) { + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() + result.append( .text( - named: "voice_settings", - value: voiceSettingsString + named: "model_id", + value: model.rawValue ) ) - } - - if let fileData = try? Data(contentsOf: audioURL) { + result.append( - .file( - named: "audio", - data: fileData, - filename: audioURL.lastPathComponent, - contentType: .mpeg + .text( + named: "language_code", + value: languageCode ) ) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { + result.append( + .text( + named: "voice_settings", + value: voiceSettingsString + ) + ) + } + + if let fileData = try? Data(contentsOf: audioURL) { + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg + ) + ) + } + + return result } - - return result } } } diff --git a/Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingOptions.swift b/Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingOptions.swift index 1ce91c86..3b7b0373 100644 --- a/Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingOptions.swift +++ b/Sources/ElevenLabs/Intramodular/Models/ElevenLabs.DubbingOptions.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 1/7/25. // +import Foundation + extension ElevenLabs.Client { public struct DubbingOptions { public var watermark: Bool? diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift index 8259acd0..9f04a260 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Chat.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Chat: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift index 3d17547d..3e45ecf6 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatEvent.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct ChatEvent: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift index 3037b590..ff0a0433 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatGroup.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct ChatGroup { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift index 1085fbbb..cb091272 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatMessage.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct ChatMessage { public let role: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift index 6353d5b2..2b6f2051 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.ChatResponse.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct ChatResponse { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift index 6cd8d926..f2debc1e 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Config.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Config: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift index ab06b510..23e315de 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Dataset.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Dataset: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift index 549e6589..56392e60 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.File.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct File: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift index b3d57c99..9f2995fa 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.FileInput.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct FileInput: Codable { public let url: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift index 5d9587f3..0231784f 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Job.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { // MARK: - Root Response public struct Job: Codable { diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift index 307c30f4..0c22b653 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Models.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Model: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift index e3cd0dcd..7cdd85c1 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Prompt.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Prompt: Codable { public let id: String diff --git a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift index 24cf1df7..c4b6b84f 100644 --- a/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift +++ b/Sources/HumeAI/Intramodular/Models/HumeAI.Tool.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/25/24. // +import Foundation + extension HumeAI { public struct Tool: Codable { public let id: String diff --git a/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift index 6c25fd1e..f48f0519 100644 --- a/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift +++ b/Sources/NeetsAI/Intramodular/API/NeetsAI.APISpecification.RequestBodies.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/22/24. // +import Foundation + extension NeetsAI.APISpecification { enum RequestBodies { struct TTSInput: Codable { diff --git a/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift b/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift index ea90cdeb..e676b451 100644 --- a/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift +++ b/Sources/PlayHT/Intramodular/Models/PlayHT.VoiceSettings.swift @@ -5,6 +5,8 @@ // Created by Jared Davidson on 11/20/24. // +import Foundation + extension PlayHT { public struct VoiceSettings: Codable, Hashable { public var speed: Double From 50c32418513f200e6b6a2432e79495116e7598eb Mon Sep 17 00:00:00 2001 From: Purav Manot Date: Mon, 2 Jun 2025 18:13:39 -0400 Subject: [PATCH 73/73] Update package --- .../Models/_Gemini.GenerationConfig.swift | 23 +++++++++++++++++++ .../_Gemini.GoogleSearchRetrieval.swift | 2 +- .../_Gemini.Client+ContentGeneration.swift | 6 +++++ .../_Gemini/Intramodular/_Gemini.Model.swift | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift b/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift index e352e195..45b147af 100644 --- a/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift +++ b/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift @@ -6,6 +6,7 @@ // import Foundation +import CorePersistence extension _Gemini { public struct GenerationConfiguration: Codable { @@ -114,3 +115,25 @@ extension _Gemini.SchemaObject: Codable { } } } + +// MARK: - Conversion + +extension _Gemini.SchemaObject { + public init(from jsonSchema: JSONSchema) { + switch jsonSchema.type { + case .object: + let mappedProperties = jsonSchema.properties?.mapValues { _Gemini.SchemaObject(from: $0) } ?? [:] + self = .object(properties: mappedProperties) + case .array: + self = .array(items: _Gemini.SchemaObject(from: jsonSchema.items ?? JSONSchema(type: .string))) + case .string: + self = .string + case .number, .integer: + self = .number + case .boolean: + self = .boolean + case nil: + self = .object(properties: [:]) + } + } +} diff --git a/Sources/_Gemini/Intramodular/Models/_Gemini.GoogleSearchRetrieval.swift b/Sources/_Gemini/Intramodular/Models/_Gemini.GoogleSearchRetrieval.swift index 775464d9..0029e5b0 100644 --- a/Sources/_Gemini/Intramodular/Models/_Gemini.GoogleSearchRetrieval.swift +++ b/Sources/_Gemini/Intramodular/Models/_Gemini.GoogleSearchRetrieval.swift @@ -15,7 +15,7 @@ extension _Gemini { public let dynamicRetrievalConfiguration: DynamicRetrievalConfiguration - public init(dynamicRetrievalConfiguration: DynamicRetrievalConfiguration) { + public init(dynamicRetrievalConfiguration: DynamicRetrievalConfiguration = .init(dynamicThreshold: 0.3)) { self.dynamicRetrievalConfiguration = dynamicRetrievalConfiguration } } diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift index 174b0e1e..7bb8f683 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift @@ -26,6 +26,7 @@ extension _Gemini.Client { public func generateContent( messages: [_Gemini.Message] = [], file: _Gemini.File? = nil, + tools: [_Gemini.Tool] = [], mimeType: HTTPMediaType? = nil, model: _Gemini.Model, configuration: _Gemini.GenerationConfiguration = configDefault @@ -58,6 +59,7 @@ extension _Gemini.Client { return try await generateContent( contents: contents, systemInstruction: systemInstruction, + tools: tools, model: model, configuration: configuration ) @@ -66,6 +68,7 @@ extension _Gemini.Client { public func generateContent( messages: [_Gemini.Message] = [], files: [_Gemini.File], + tools: [_Gemini.Tool] = [], model: _Gemini.Model, configuration: _Gemini.GenerationConfiguration = configDefault ) async throws -> _Gemini.Content { @@ -96,6 +99,7 @@ extension _Gemini.Client { return try await generateContent( contents: contents, systemInstruction: systemInstruction, + tools: tools, model: model, configuration: configuration ) @@ -104,6 +108,7 @@ extension _Gemini.Client { internal func generateContent( contents: [_Gemini.APISpecification.RequestBodies.Content], systemInstruction: _Gemini.APISpecification.RequestBodies.Content?, + tools: [_Gemini.Tool] = [], model: _Gemini.Model, configuration: _Gemini.GenerationConfiguration ) async throws -> _Gemini.Content { @@ -112,6 +117,7 @@ extension _Gemini.Client { requestBody: .init( contents: contents, generationConfig: configuration, + tools: tools, systemInstruction: systemInstruction ) ) diff --git a/Sources/_Gemini/Intramodular/_Gemini.Model.swift b/Sources/_Gemini/Intramodular/_Gemini.Model.swift index 672dc5e0..bf217489 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Model.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Model.swift @@ -23,6 +23,7 @@ extension _Gemini { self.rawValue = rawValue } + public static let gemini_2_0_flash = Model(rawValue: "gemini-2.0-flash") public static let gemini_2_0_flash_exp = Model(rawValue: "gemini-2.0-flash-exp") public static let gemini_1_5_pro = Model(rawValue: "gemini-1.5-pro") public static let gemini_1_5_pro_latest = Model(rawValue: "gemini-1.5-pro-latest")