├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md └── Sources └── ElevenlabsSwift └── ElevenlabsSwift.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ElevenlabsSwift", 8 | platforms: [ 9 | .iOS(.v14), .tvOS(.v14) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "ElevenlabsSwift", 15 | targets: ["ElevenlabsSwift"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "ElevenlabsSwift", 26 | dependencies: []), 27 | 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElevenlabsSwift 2 | 3 | ElevenlabsSwift is an open-source Swift package that provides an easy-to-use API for managing and utilizing voices in the VoiceLab, a cloud-based service for creating and managing custom voice collections. With ElevenlabsSwift, you can convert text into speech, get a list of all available voices, delete or add new voices, and even edit existing voices created by you. 4 | 5 | ## Features 6 | 7 | ElevenlabsSwift API includes the following features: 8 | 9 | - Convert text into speech using a voice of your choice and return the audio. 10 | - Get a list of all available voices for a user. 11 | - Delete a voice by its ID. 12 | - Add a new voice to your collection of voices in VoiceLab. 13 | - Edit a voice created by you. 14 | 15 | ## Example of usage 16 | 17 | ```swift 18 | import ElevenlabsSwift 19 | 20 | let elevenApi = ElevenlabsSwift(elevenLabsAPI: Elevenlabs_API_key) 21 | let url = try await elevenApi.textToSpeech(voice_id: selectedVoice.voice_id, text: "text to speech") 22 | ``` 23 | 24 | ## Installation 25 | 26 | To use ElevenlabsSwift in your project, add it to your Package.swift file: 27 | 28 | ```swift 29 | dependencies: [ 30 | .package(url: "https://github.com/ArchieGoodwin/ElevenlabsSwift", from: "0.7.2") 31 | ] 32 | ``` 33 | 34 | And then import the package in your source files: 35 | 36 | ```swift 37 | import ElevenlabsSwift 38 | ``` 39 | 40 | ## License 41 | 42 | MIT License 43 | 44 | Copyright (c) 2023 wilder.dev LLC 45 | 46 | Permission is hereby granted, free of charge, to any person obtaining a copy 47 | of this software and associated documentation files (the "Software"), to deal 48 | in the Software without restriction, including without limitation the rights 49 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 50 | copies of the Software, and to permit persons to whom the Software is 51 | furnished to do so, subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all 54 | copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 57 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 58 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 59 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 60 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 61 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 62 | SOFTWARE. 63 | 64 | -------------------------------------------------------------------------------- /Sources/ElevenlabsSwift/ElevenlabsSwift.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public class ElevenlabsSwift { 5 | private var elevenLabsAPI: String 6 | 7 | public required init(elevenLabsAPI: String) { 8 | self.elevenLabsAPI = elevenLabsAPI 9 | } 10 | 11 | private let baseURL = "https://api.elevenlabs.io" 12 | 13 | public func fetchVoices() async throws -> [Voice] 14 | { 15 | 16 | let session = URLSession.shared 17 | let url = URL(string: "\(baseURL)/v1/voices")! 18 | var request = URLRequest(url: url) 19 | request.httpMethod = "GET" 20 | request.setValue(elevenLabsAPI, forHTTPHeaderField: "xi-api-key") 21 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 22 | 23 | do { 24 | let (data, _) = try await session.data(for: request) 25 | 26 | let userResponse: VoicesResponse = try JSONDecoder().decode(VoicesResponse.self, from: data) 27 | print(userResponse.voices) 28 | 29 | return userResponse.voices 30 | } 31 | catch(let error) 32 | { 33 | throw WebAPIError.httpError(message: error.localizedDescription) 34 | } 35 | 36 | } 37 | 38 | public func textToSpeech(voice_id: String, text: String, model: String? = nil) async throws -> URL 39 | { 40 | 41 | let session = URLSession.shared 42 | let url = URL(string: "\(baseURL)/v1/text-to-speech/\(voice_id)")! 43 | var request = URLRequest(url: url) 44 | request.httpMethod = "POST" 45 | request.setValue(elevenLabsAPI, forHTTPHeaderField: "xi-api-key") 46 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 47 | request.setValue("audio/mpeg", forHTTPHeaderField: "Accept") 48 | 49 | let parameters: SpeechRequest = SpeechRequest(text: text, voice_settings: ["stability" : 0, "similarity_boost": 0], model: model) 50 | 51 | guard let jsonBody = try? JSONEncoder().encode(parameters) else { 52 | throw WebAPIError.unableToEncodeJSONData 53 | } 54 | 55 | request.httpBody = jsonBody 56 | 57 | do { 58 | let (data, _) = try await session.data(for: request) 59 | print(data) 60 | 61 | let url = try self.saveDataToTempFile(data: data) 62 | 63 | return url 64 | } 65 | catch(let error) 66 | { 67 | throw WebAPIError.httpError(message: error.localizedDescription) 68 | } 69 | 70 | } 71 | 72 | private func saveDataToTempFile(data: Data) throws -> URL { 73 | let tempDirectoryURL = FileManager.default.temporaryDirectory 74 | let randomFilename = "\(UUID().uuidString).mpg" 75 | let fileURL = tempDirectoryURL.appendingPathComponent(randomFilename) 76 | print(data.count) 77 | try data.write(to: fileURL) 78 | return fileURL 79 | } 80 | 81 | // Utility function to create data for multipart/form-data 82 | private func createMultipartData(boundary: String, name: String, fileURL: URL, fileType: String) -> Data? { 83 | let fileName = fileURL.lastPathComponent 84 | var data = Data() 85 | 86 | // Multipart form data header 87 | data.append("--\(boundary)\r\n".data(using: .utf8)!) 88 | data.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) 89 | data.append("Content-Type: \(fileType)\r\n\r\n".data(using: .utf8)!) 90 | 91 | // File content 92 | guard let fileData = try? Data(contentsOf: fileURL) else { 93 | return nil 94 | } 95 | data.append(fileData) 96 | data.append("\r\n".data(using: .utf8)!) 97 | 98 | return data 99 | } 100 | 101 | public func uploadVoice(name: String, description: String, fileURL: URL, completion: @escaping (String?) -> Void) { 102 | 103 | guard let url = URL(string: "\(baseURL)/v1/voices/add") else { 104 | print("Invalid URL") 105 | completion(nil) 106 | return 107 | } 108 | 109 | let boundary = UUID().uuidString 110 | var request = URLRequest(url: url) 111 | request.httpMethod = "POST" 112 | request.setValue("application/json", forHTTPHeaderField: "accept") 113 | request.setValue(elevenLabsAPI, forHTTPHeaderField: "xi-api-key") 114 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 115 | 116 | var data = Data() 117 | let parameters = [ 118 | ("name", name), 119 | ("description", description), 120 | ("labels", "") 121 | ] 122 | 123 | for (key, value) in parameters { 124 | data.append("--\(boundary)\r\n".data(using: .utf8)!) 125 | data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) 126 | data.append("\(value)\r\n".data(using: .utf8)!) 127 | } 128 | 129 | if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") { 130 | data.append(fileData) 131 | } 132 | 133 | // Multipart form data footer 134 | data.append("--\(boundary)--\r\n".data(using: .utf8)!) 135 | 136 | request.httpBody = data 137 | 138 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 139 | if let error = error { 140 | print("Error: \(error)") 141 | completion(nil) 142 | } else if let data = data { 143 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { 144 | do { 145 | if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] { 146 | print(json) 147 | completion(json["voice_id"]) 148 | } else { 149 | completion(nil) 150 | } 151 | 152 | } catch { 153 | print("Error decoding JSON: \(error)") 154 | completion(nil) 155 | } 156 | } else { 157 | print("Error: Invalid status code") 158 | completion(nil) 159 | } 160 | } 161 | } 162 | 163 | task.resume() 164 | } 165 | 166 | public func deleteVoice(voiceId: String) async throws { 167 | guard let url = URL(string: "\(baseURL)/v1/voices/\(voiceId)") else { 168 | print("Invalid URL") 169 | throw WebAPIError.httpError(message: "incorrect url") 170 | } 171 | let session = URLSession.shared 172 | 173 | var request = URLRequest(url: url) 174 | request.httpMethod = "DELETE" 175 | request.setValue("application/json", forHTTPHeaderField: "accept") 176 | request.setValue(elevenLabsAPI, forHTTPHeaderField: "xi-api-key") 177 | 178 | do { 179 | let (data, _) = try await session.data(for: request) 180 | 181 | } 182 | catch(let error) 183 | { 184 | throw WebAPIError.httpError(message: error.localizedDescription) 185 | } 186 | } 187 | 188 | public func editVoice(voiceId: String, name: String, description: String, fileURL: URL, completion: @escaping (Bool) -> Void) { 189 | 190 | guard let url = URL(string: "\(baseURL)/v1/voices/\(voiceId)/edit") else { 191 | print("Invalid URL") 192 | completion(false) 193 | return 194 | } 195 | 196 | let boundary = UUID().uuidString 197 | var request = URLRequest(url: url) 198 | request.httpMethod = "POST" 199 | request.setValue("application/json", forHTTPHeaderField: "accept") 200 | request.setValue(elevenLabsAPI, forHTTPHeaderField: "xi-api-key") 201 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 202 | 203 | var data = Data() 204 | let parameters = [ 205 | ("name", name), 206 | ("description", description), 207 | ("labels", "") 208 | ] 209 | 210 | for (key, value) in parameters { 211 | data.append("--\(boundary)\r\n".data(using: .utf8)!) 212 | data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) 213 | data.append("\(value)\r\n".data(using: .utf8)!) 214 | } 215 | 216 | if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") { 217 | data.append(fileData) 218 | } 219 | 220 | // Multipart form data footer 221 | data.append("--\(boundary)--\r\n".data(using: .utf8)!) 222 | 223 | request.httpBody = data 224 | 225 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 226 | if let error = error { 227 | print("Error: \(error)") 228 | completion(false) 229 | } else if let data = data { 230 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { 231 | do { 232 | let json = try JSONSerialization.jsonObject(with: data, options: []) 233 | print(json) 234 | completion(true) 235 | } catch { 236 | print("Error decoding JSON: \(error)") 237 | completion(false) 238 | } 239 | } else { 240 | print("Error: Invalid status code") 241 | completion(false) 242 | } 243 | } 244 | } 245 | 246 | task.resume() 247 | } 248 | 249 | 250 | } 251 | 252 | 253 | public enum WebAPIError: Error { 254 | case identityTokenMissing 255 | case unableToDecodeIdentityToken 256 | case unableToEncodeJSONData 257 | case unableToDecodeJSONData 258 | case unauthorized 259 | case invalidResponse 260 | case httpError(message: String) 261 | case httpErrorWithStatus(status: Int) 262 | 263 | } 264 | 265 | 266 | public struct VoicesResponse: Codable { 267 | public let voices: [Voice] 268 | 269 | public init(voices: [Voice]) { 270 | self.voices = voices 271 | } 272 | } 273 | 274 | 275 | public struct Voice: Codable, Identifiable, Hashable { 276 | public let voice_id: String 277 | public let name: String 278 | 279 | public var id: String { voice_id } 280 | 281 | public init(voice_id: String, name: String) { 282 | self.voice_id = voice_id 283 | self.name = name 284 | } 285 | } 286 | 287 | 288 | public struct SpeechRequest: Codable { 289 | public let text: String 290 | public let voice_settings: [String: Int] 291 | public let model: String? 292 | 293 | public init(text: String, voice_settings: [String : Int], model: String?) { 294 | self.text = text 295 | self.voice_settings = voice_settings 296 | self.model = model 297 | } 298 | } 299 | --------------------------------------------------------------------------------