Skip to content

Commit 6657459

Browse files
added Client namespace for ElevenLabs
1 parent 2af08d5 commit 6657459

File tree

3 files changed

+248
-234
lines changed

3 files changed

+248
-234
lines changed

Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ extension ElevenLabs {
1414
}
1515

1616
extension ElevenLabs.APISpecification {
17-
public enum RequestBodies {
17+
enum RequestBodies {
1818
public struct SpeechRequest: Codable {
1919
public enum CodingKeys: String, CodingKey {
2020
case text
2121
case voiceSettings
2222
case model
2323
}
2424

25-
public let text: String
26-
public let voiceSettings: [String: JSON]
27-
public let model: ElevenLabs.Model
25+
let text: String
26+
let voiceSettings: [String: JSON]
27+
let model: ElevenLabs.Client.Model
2828

29-
public init(
29+
init(
3030
text: String,
3131
voiceSettings: [String: JSON],
32-
model: ElevenLabs.Model
32+
model: ElevenLabs.Client.Model
3333
) {
3434
self.text = text
3535
self.voiceSettings = voiceSettings
@@ -42,16 +42,16 @@ extension ElevenLabs.APISpecification {
4242
extension ElevenLabs.APISpecification {
4343
public enum ResponseBodies {
4444
public struct Voices: Codable, Hashable, Sendable {
45-
public let voices: [ElevenLabs.Voice]
45+
public let voices: [ElevenLabs.Client.Voice]
4646

47-
public init(voices: [ElevenLabs.Voice]) {
47+
public init(voices: [ElevenLabs.Client.Voice]) {
4848
self.voices = voices
4949
}
5050
}
5151
}
5252
}
5353

54-
extension ElevenLabs {
54+
extension ElevenLabs.Client {
5555
public struct Voice: Codable, Hashable, Identifiable, Sendable {
5656
public typealias ID = _TypeAssociatedID<Self, String>
5757

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
//
2+
// Copyright (c) Vatsal Manot
3+
//
4+
5+
import CoreMI
6+
import CorePersistence
7+
import Foundation
8+
import NetworkKit
9+
10+
11+
extension ElevenLabs {
12+
public final class Client: ObservableObject {
13+
14+
public struct Configuration {
15+
public var apiKey: String?
16+
}
17+
18+
public let configuration: Configuration
19+
public let apiSpecification = APISpecification()
20+
21+
public required init(
22+
configuration: Configuration
23+
) {
24+
self.configuration = configuration
25+
}
26+
27+
public convenience init(
28+
apiKey: String?
29+
) {
30+
self.init(configuration: .init(apiKey: apiKey))
31+
}
32+
}
33+
}
34+
35+
extension ElevenLabs.Client {
36+
public func availableVoices() async throws -> [Voice] {
37+
let request = HTTPRequest(url: URL(string: "\(apiSpecification)/v1/voices")!)
38+
.method(.get)
39+
.header("xi-api-key", configuration.apiKey)
40+
.header(.contentType(.json))
41+
42+
let response = try await HTTPSession.shared.data(for: request)
43+
44+
try response.validate()
45+
46+
return try response.decode(
47+
ElevenLabs.APISpecification.ResponseBodies.Voices.self,
48+
keyDecodingStrategy: .convertFromSnakeCase
49+
)
50+
.voices
51+
}
52+
53+
@discardableResult
54+
public func speech(
55+
for text: String,
56+
voiceID: String,
57+
voiceSettings: [String: JSON]? = nil,
58+
model: ElevenLabs.Client.Model
59+
) async throws -> Data {
60+
let request = try HTTPRequest(url: URL(string: "\(apiSpecification.host)/v1/text-to-speech/\(voiceID)")!)
61+
.method(.post)
62+
.header("xi-api-key", configuration.apiKey)
63+
.header(.contentType(.json))
64+
.header(.accept(.mpeg))
65+
.jsonBody(
66+
ElevenLabs.APISpecification.RequestBodies.SpeechRequest(
67+
text: text,
68+
voiceSettings: voiceSettings ?? [
69+
"stability" : 0,
70+
"similarity_boost": 0
71+
],
72+
model: model
73+
),
74+
keyEncodingStrategy: .convertToSnakeCase
75+
)
76+
77+
let response = try await HTTPSession.shared.data(for: request)
78+
79+
do {
80+
try response.validate()
81+
} catch {
82+
response.data
83+
}
84+
85+
return response.data
86+
}
87+
88+
public func upload(
89+
voiceWithName name: String,
90+
description: String,
91+
fileURL: URL
92+
) async throws -> Voice.ID {
93+
let boundary = UUID().uuidString
94+
95+
var request = try URLRequest(url: URL(string: "\(apiSpecification.host)/v1/voices/add").unwrap())
96+
97+
request.httpMethod = "POST"
98+
request.setValue("application/json", forHTTPHeaderField: "accept")
99+
request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key")
100+
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
101+
102+
var data = Data()
103+
let parameters = [
104+
("name", name),
105+
("description", description),
106+
("labels", "")
107+
]
108+
109+
for (key, value) in parameters {
110+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
111+
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
112+
data.append("\(value)\r\n".data(using: .utf8)!)
113+
}
114+
115+
if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") {
116+
data.append(fileData)
117+
}
118+
119+
data.append("--\(boundary)--\r\n".data(using: .utf8)!)
120+
121+
request.httpBody = data
122+
123+
let voiceID: String? = try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<String?, Error>) in
124+
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
125+
if let error = error {
126+
continuation.resume(throwing: error)
127+
} else if let data = data {
128+
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
129+
do {
130+
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
131+
continuation.resume(returning: json["voice_id"])
132+
} else {
133+
continuation.resume(returning: nil)
134+
}
135+
136+
} catch {
137+
continuation.resume(throwing: _PlaceholderError())
138+
}
139+
} else {
140+
continuation.resume(throwing: _PlaceholderError())
141+
}
142+
}
143+
}
144+
145+
task.resume()
146+
}
147+
148+
return try .init(rawValue: voiceID.unwrap())
149+
}
150+
151+
public func edit(
152+
voice: Voice.ID,
153+
name: String,
154+
description: String,
155+
fileURL: URL
156+
) async throws -> Bool {
157+
let url = URL(string: "\(apiSpecification.host)/v1/voices/\(voice.rawValue)/edit")!
158+
159+
let boundary = UUID().uuidString
160+
var request = URLRequest(url: url)
161+
162+
request.httpMethod = "POST"
163+
request.setValue("application/json", forHTTPHeaderField: "accept")
164+
request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key")
165+
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
166+
167+
var data = Data()
168+
let parameters = [
169+
("name", name),
170+
("description", description),
171+
("labels", "")
172+
]
173+
174+
for (key, value) in parameters {
175+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
176+
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
177+
data.append("\(value)\r\n".data(using: .utf8)!)
178+
}
179+
180+
if let fileData = createMultipartData(boundary: boundary, name: "files", fileURL: fileURL, fileType: "audio/x-wav") {
181+
data.append(fileData)
182+
}
183+
184+
data.append("--\(boundary)--\r\n".data(using: .utf8)!)
185+
186+
request.httpBody = data
187+
188+
let response = try await HTTPSession.shared.data(for: request)
189+
190+
try response.validate()
191+
192+
return true
193+
}
194+
195+
public func delete(
196+
voice: Voice.ID
197+
) async throws {
198+
let url = try URL(string: "\(apiSpecification.host)/v1/voices/\(voice.rawValue)").unwrap()
199+
200+
var request = URLRequest(url: url)
201+
202+
request.httpMethod = "DELETE"
203+
request.setValue("application/json", forHTTPHeaderField: "accept")
204+
request.setValue(configuration.apiKey, forHTTPHeaderField: "xi-api-key")
205+
206+
let response = try await HTTPSession.shared.data(for: request)
207+
208+
try response.validate()
209+
}
210+
211+
private func createMultipartData(
212+
boundary: String,
213+
name: String,
214+
fileURL: URL,
215+
fileType: String
216+
) -> Data? {
217+
var result = Data()
218+
let fileName = fileURL.lastPathComponent
219+
220+
result.append("--\(boundary)\r\n".data(using: .utf8)!)
221+
result.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
222+
result.append("Content-Type: \(fileType)\r\n\r\n".data(using: .utf8)!)
223+
224+
guard let fileData = try? Data(contentsOf: fileURL) else {
225+
return nil
226+
}
227+
228+
result.append(fileData)
229+
result.append("\r\n".data(using: .utf8)!)
230+
231+
return result
232+
}
233+
}

0 commit comments

Comments
 (0)