Skip to content

Commit 8b1862a

Browse files
committed
2 parents 68b3a3f + 63e9403 commit 8b1862a

14 files changed

+422
-361
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ import Mistral
9898
// OpenAI / GPT
9999
let client: any LLMRequestHandling = OpenAI.Client(apiKey: "YOUR_KEY")
100100
// Anthropic / Claude
101-
let client: any LLMRequestHandling = Anthropic(apiKey: "YOUR_KEY")
101+
let client: any LLMRequestHandling = Anthropic.Client(apiKey: "YOUR_KEY")
102102
// Mistral
103-
let client: any LLMRequestHandling = Mistral(apiKey: "YOUR_KEY")
103+
let client: any LLMRequestHandling = Mistral.Client(apiKey: "YOUR_KEY")
104104
```
105105

106106
You can now use `client` as an interface to an LLM as provided by the underlying provider.

Sources/ElevenLabs/Intramodular/ElevenLabs.APISpecification.swift

+5-40
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ 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.Model
2828

29-
public init(
29+
init(
3030
text: String,
3131
voiceSettings: [String: JSON],
3232
model: ElevenLabs.Model
@@ -50,38 +50,3 @@ extension ElevenLabs.APISpecification {
5050
}
5151
}
5252
}
53-
54-
extension ElevenLabs {
55-
public struct Voice: Codable, Hashable, Identifiable, Sendable {
56-
public typealias ID = _TypeAssociatedID<Self, String>
57-
58-
public enum CodingKeys: String, CodingKey {
59-
case voiceID = "voiceId"
60-
case name
61-
}
62-
63-
public let voiceID: String
64-
public let name: String
65-
66-
public var id: ID {
67-
ID(rawValue: voiceID)
68-
}
69-
70-
public init(voiceID: String, name: String) {
71-
self.voiceID = voiceID
72-
self.name = name
73-
}
74-
}
75-
76-
public enum Model: String, Codable, Sendable {
77-
// Information about each model here: https://help.elevenlabs.io/hc/en-us/articles/17883183930129-What-models-do-you-offer-and-what-is-the-difference-between-them
78-
// Using cutting-edge technology, this is a highly optimized model for real-time applications that require very low latency, but it still retains the fantastic quality offered in our other models. Even if optimized for real-time and more conversational applications, we still recommend testing it out for other applications as it is very versatile and stable.
79-
case TurboV2 = "eleven_turbo_v2"
80-
/// This model is a powerhouse, excelling in stability, language diversity, and accuracy in replicating accents and voices. Its speed and agility are remarkable considering its size. Multilingual v2 supports a 28 languages.
81-
case MultilingualV2 = "eleven_multilingual_v2"
82-
/// This model was created specifically for English and is the smallest and fastest model we offer. As our oldest model, it has undergone extensive optimization to ensure reliable performance but it is also the most limited and generally the least accurate.
83-
case EnglishV1 = "eleven_monolingual_v1"
84-
/// Taking a step towards global access and usage, we introduced Multilingual v1 as our second offering. Has been an experimental model ever since release. To this day, it still remains in the experimental phase.
85-
case MultilingualV1 = "eleven_multilingual_v1"
86-
}
87-
}
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 -> [ElevenLabs.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.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 -> ElevenLabs.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: ElevenLabs.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: ElevenLabs.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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Natasha Murashev on 5/28/24.
6+
//
7+
8+
import Foundation
9+
10+
extension ElevenLabs {
11+
public enum Model: String, Codable, Sendable {
12+
// Information about each model here: https://help.elevenlabs.io/hc/en-us/articles/17883183930129-What-models-do-you-offer-and-what-is-the-difference-between-them
13+
// Using cutting-edge technology, this is a highly optimized model for real-time applications that require very low latency, but it still retains the fantastic quality offered in our other models. Even if optimized for real-time and more conversational applications, we still recommend testing it out for other applications as it is very versatile and stable.
14+
case TurboV2 = "eleven_turbo_v2"
15+
/// This model is a powerhouse, excelling in stability, language diversity, and accuracy in replicating accents and voices. Its speed and agility are remarkable considering its size. Multilingual v2 supports a 28 languages.
16+
case MultilingualV2 = "eleven_multilingual_v2"
17+
/// This model was created specifically for English and is the smallest and fastest model we offer. As our oldest model, it has undergone extensive optimization to ensure reliable performance but it is also the most limited and generally the least accurate.
18+
case EnglishV1 = "eleven_monolingual_v1"
19+
/// Taking a step towards global access and usage, we introduced Multilingual v1 as our second offering. Has been an experimental model ever since release. To this day, it still remains in the experimental phase.
20+
case MultilingualV1 = "eleven_multilingual_v1"
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Natasha Murashev on 5/28/24.
6+
//
7+
8+
import Foundation
9+
10+
extension ElevenLabs {
11+
public struct Voice: Codable, Hashable, Identifiable, Sendable {
12+
public typealias ID = _TypeAssociatedID<Self, String>
13+
14+
public enum CodingKeys: String, CodingKey {
15+
case voiceID = "voiceId"
16+
case name
17+
}
18+
19+
public let voiceID: String
20+
public let name: String
21+
22+
public var id: ID {
23+
ID(rawValue: voiceID)
24+
}
25+
26+
public init(voiceID: String, name: String) {
27+
self.voiceID = voiceID
28+
self.name = name
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)