eleven_rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +21 -0
- data/README.md +311 -0
- data/lib/eleven_rb/callbacks.rb +47 -0
- data/lib/eleven_rb/client.rb +110 -0
- data/lib/eleven_rb/collections/base.rb +92 -0
- data/lib/eleven_rb/collections/library_voice_collection.rb +91 -0
- data/lib/eleven_rb/collections/voice_collection.rb +78 -0
- data/lib/eleven_rb/configuration.rb +87 -0
- data/lib/eleven_rb/errors.rb +58 -0
- data/lib/eleven_rb/http/client.rb +277 -0
- data/lib/eleven_rb/instrumentation.rb +35 -0
- data/lib/eleven_rb/objects/audio.rb +118 -0
- data/lib/eleven_rb/objects/base.rb +86 -0
- data/lib/eleven_rb/objects/cost_info.rb +72 -0
- data/lib/eleven_rb/objects/library_voice.rb +66 -0
- data/lib/eleven_rb/objects/model.rb +56 -0
- data/lib/eleven_rb/objects/subscription.rb +91 -0
- data/lib/eleven_rb/objects/user_info.rb +24 -0
- data/lib/eleven_rb/objects/voice.rb +86 -0
- data/lib/eleven_rb/objects/voice_settings.rb +41 -0
- data/lib/eleven_rb/resources/base.rb +84 -0
- data/lib/eleven_rb/resources/models.rb +65 -0
- data/lib/eleven_rb/resources/text_to_speech.rb +164 -0
- data/lib/eleven_rb/resources/user.rb +66 -0
- data/lib/eleven_rb/resources/voice_library.rb +160 -0
- data/lib/eleven_rb/resources/voices.rb +138 -0
- data/lib/eleven_rb/tts_adapter.rb +151 -0
- data/lib/eleven_rb/version.rb +5 -0
- data/lib/eleven_rb/voice_slot_manager.rb +184 -0
- data/lib/eleven_rb.rb +113 -0
- metadata +193 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2402cc8af1a30f52cf74017c0f24adc423ae932616fe74296e1fe09a635c628b
|
|
4
|
+
data.tar.gz: f149ab57fc621a178b5e2b4443a6cc02fb830769e2053152b86b246dbc8ca2cb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 42df0d9c9095233697e41aa08e98297090fc1def3f5e16c4c3f248ddd60650d5be8fb85456ab41b352c92bc3dbd8208041010764a61b7143bdcfda706b890b7f
|
|
7
|
+
data.tar.gz: 50ba905bccdddb14a8148345f69461a98cdc58659e1a6a3c75098de02744962a76d354fcd48326fc29722c3d03a81c9cd426be47706f5e5eb0d91faf38a4161b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-01-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release
|
|
15
|
+
- Text-to-Speech generation with `client.tts.generate`
|
|
16
|
+
- Streaming TTS with `client.tts.stream`
|
|
17
|
+
- Voice management (list, get, create, update, delete)
|
|
18
|
+
- Voice Library access (search, add shared voices)
|
|
19
|
+
- Voice Slot Manager for automatic slot management
|
|
20
|
+
- Models resource for listing available TTS models
|
|
21
|
+
- User/subscription information
|
|
22
|
+
- Comprehensive callback system:
|
|
23
|
+
- `on_request` - before each API call
|
|
24
|
+
- `on_response` - after successful response
|
|
25
|
+
- `on_error` - when errors occur
|
|
26
|
+
- `on_audio_generated` - after TTS generation (includes cost info)
|
|
27
|
+
- `on_retry` - before retry attempts
|
|
28
|
+
- `on_rate_limit` - when rate limited
|
|
29
|
+
- `on_voice_added` / `on_voice_deleted` - voice changes
|
|
30
|
+
- Automatic retry with exponential backoff
|
|
31
|
+
- Structured response objects
|
|
32
|
+
- TTSAdapter for future wrapper gem compatibility
|
|
33
|
+
- ActiveSupport::Notifications integration (optional)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Web Ventures Ltd - www.webven.nz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# ElevenRb
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/eleven_rb)
|
|
4
|
+
[](https://github.com/webventures/eleven_rb/actions/workflows/ci.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A Ruby client for the [ElevenLabs](https://try.elevenlabs.io/qyk2j8gumrjz) Text-to-Speech API.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Text-to-Speech generation and streaming
|
|
12
|
+
- Voice management (list, get, create, update, delete)
|
|
13
|
+
- Voice Library access (search 10,000+ community voices)
|
|
14
|
+
- Voice Slot Manager for automatic slot management within account limits
|
|
15
|
+
- Comprehensive callback system for logging, monitoring, and cost tracking
|
|
16
|
+
- Automatic retry with configurable backoff
|
|
17
|
+
- Structured response objects
|
|
18
|
+
- Future-ready adapter for multi-provider wrapper gems
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby >= 3.0
|
|
23
|
+
- An [ElevenLabs API key](https://elevenlabs.io/app/settings/api-keys)
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add this line to your application's Gemfile:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem 'eleven_rb'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install directly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install eleven_rb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'eleven_rb'
|
|
43
|
+
|
|
44
|
+
# Initialize with API key
|
|
45
|
+
client = ElevenRb::Client.new(api_key: "your-api-key")
|
|
46
|
+
|
|
47
|
+
# Or use environment variable ELEVENLABS_API_KEY
|
|
48
|
+
client = ElevenRb::Client.new
|
|
49
|
+
|
|
50
|
+
# Generate speech
|
|
51
|
+
audio = client.tts.generate("Hello world!", voice_id: "JBFqnCBsd6RMkjVDRZzb")
|
|
52
|
+
audio.save_to_file("output.mp3")
|
|
53
|
+
|
|
54
|
+
# List voices
|
|
55
|
+
client.voices.list.each do |voice|
|
|
56
|
+
puts "#{voice.name} (#{voice.voice_id})"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Text-to-Speech
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# Basic generation
|
|
66
|
+
audio = client.tts.generate("Hello world", voice_id: "voice_id")
|
|
67
|
+
audio.save_to_file("output.mp3")
|
|
68
|
+
|
|
69
|
+
# With options
|
|
70
|
+
audio = client.tts.generate(
|
|
71
|
+
"Hello world",
|
|
72
|
+
voice_id: "voice_id",
|
|
73
|
+
model_id: "eleven_multilingual_v2",
|
|
74
|
+
voice_settings: {
|
|
75
|
+
stability: 0.5,
|
|
76
|
+
similarity_boost: 0.75
|
|
77
|
+
},
|
|
78
|
+
output_format: "mp3_44100_192"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Streaming
|
|
82
|
+
File.open("output.mp3", "wb") do |file|
|
|
83
|
+
client.tts.stream("Long text here...", voice_id: "voice_id") do |chunk|
|
|
84
|
+
file.write(chunk)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Voice Management
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# List all voices
|
|
93
|
+
voices = client.voices.list
|
|
94
|
+
voices.each { |v| puts v.display_name }
|
|
95
|
+
|
|
96
|
+
# Get a specific voice
|
|
97
|
+
voice = client.voices.find("voice_id")
|
|
98
|
+
puts voice.name
|
|
99
|
+
|
|
100
|
+
# Delete a voice
|
|
101
|
+
client.voices.destroy("voice_id")
|
|
102
|
+
|
|
103
|
+
# Filter voices
|
|
104
|
+
spanish_voices = voices.by_language("spanish")
|
|
105
|
+
female_voices = voices.by_gender("female")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Voice Library
|
|
109
|
+
|
|
110
|
+
Search and add voices from ElevenLabs' 10,000+ community voice library:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Search for Spanish female voices
|
|
114
|
+
results = client.voice_library.search(
|
|
115
|
+
language: "Spanish",
|
|
116
|
+
gender: "female",
|
|
117
|
+
page_size: 20
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
results.each do |voice|
|
|
121
|
+
puts "#{voice.name} - #{voice.accent}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Add a voice from the library to your account
|
|
125
|
+
voice = client.voice_library.add(
|
|
126
|
+
public_user_id: voice.public_owner_id,
|
|
127
|
+
voice_id: voice.voice_id,
|
|
128
|
+
name: "My Spanish Voice"
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Voice Slot Management
|
|
133
|
+
|
|
134
|
+
Automatically manage voice slots when you're limited by your subscription:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Check current slot status
|
|
138
|
+
status = client.voice_slots.status
|
|
139
|
+
puts "#{status[:used]}/#{status[:limit]} slots used"
|
|
140
|
+
|
|
141
|
+
# Ensure a voice is available (adds from library if needed, removes LRU if full)
|
|
142
|
+
voice = client.voice_slots.ensure_available(
|
|
143
|
+
public_user_id: "owner_id",
|
|
144
|
+
voice_id: "voice_id",
|
|
145
|
+
name: "Spanish Voice"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Now use the voice
|
|
149
|
+
audio = client.tts.generate("Hola mundo", voice_id: voice.voice_id)
|
|
150
|
+
|
|
151
|
+
# Prepare multiple voices for a conversation
|
|
152
|
+
voices = client.voice_slots.prepare_voices([
|
|
153
|
+
{ public_user_id: "abc", voice_id: "v1", name: "Maria" },
|
|
154
|
+
{ public_user_id: "def", voice_id: "v2", name: "Carlos" }
|
|
155
|
+
])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Callbacks
|
|
159
|
+
|
|
160
|
+
Set up callbacks for logging, monitoring, and cost tracking:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
client = ElevenRb::Client.new(
|
|
164
|
+
api_key: ENV['ELEVENLABS_API_KEY'],
|
|
165
|
+
|
|
166
|
+
# Logging
|
|
167
|
+
on_request: ->(method:, path:, body:) {
|
|
168
|
+
Rails.logger.info("[ElevenLabs] #{method.upcase} #{path}")
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
on_response: ->(method:, path:, response:, duration:) {
|
|
172
|
+
Rails.logger.info("[ElevenLabs] #{method.upcase} #{path} (#{duration}ms)")
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
# Error tracking
|
|
176
|
+
on_error: ->(error:, method:, path:, context:) {
|
|
177
|
+
Sentry.capture_exception(error, extra: { path: path })
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
# Cost tracking
|
|
181
|
+
on_audio_generated: ->(audio:, voice_id:, text:, cost_info:) {
|
|
182
|
+
UsageRecord.create!(
|
|
183
|
+
characters: cost_info[:character_count],
|
|
184
|
+
estimated_cost: cost_info[:estimated_cost]
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
# Rate limit handling
|
|
189
|
+
on_rate_limit: ->(retry_after:, error:) {
|
|
190
|
+
SlackNotifier.notify("Rate limited, retry in #{retry_after}s")
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Models
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
# List available models
|
|
199
|
+
models = client.models.list
|
|
200
|
+
models.each { |m| puts "#{m.name} (#{m.model_id})" }
|
|
201
|
+
|
|
202
|
+
# Get multilingual models
|
|
203
|
+
client.models.multilingual
|
|
204
|
+
|
|
205
|
+
# Get turbo/fast models
|
|
206
|
+
client.models.turbo
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### User Information
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# Get subscription info
|
|
213
|
+
sub = client.user.subscription
|
|
214
|
+
puts "Characters: #{sub.character_count}/#{sub.character_limit}"
|
|
215
|
+
puts "Resets at: #{sub.next_reset_at}"
|
|
216
|
+
|
|
217
|
+
# Get user info
|
|
218
|
+
info = client.user.info
|
|
219
|
+
puts "Email: #{info.email}"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Configuration
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
client = ElevenRb::Client.new(
|
|
226
|
+
api_key: "your-api-key",
|
|
227
|
+
timeout: 120, # Request timeout in seconds
|
|
228
|
+
open_timeout: 10, # Connection timeout
|
|
229
|
+
max_retries: 3, # Max retry attempts
|
|
230
|
+
retry_delay: 1.0, # Base delay between retries
|
|
231
|
+
logger: Rails.logger # Optional logger
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Error Handling
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
begin
|
|
239
|
+
audio = client.tts.generate("Hello", voice_id: "invalid")
|
|
240
|
+
rescue ElevenRb::Errors::NotFoundError => e
|
|
241
|
+
puts "Voice not found: #{e.message}"
|
|
242
|
+
rescue ElevenRb::Errors::RateLimitError => e
|
|
243
|
+
puts "Rate limited, retry after #{e.retry_after} seconds"
|
|
244
|
+
rescue ElevenRb::Errors::AuthenticationError => e
|
|
245
|
+
puts "Invalid API key"
|
|
246
|
+
rescue ElevenRb::Errors::ValidationError => e
|
|
247
|
+
puts "Validation error: #{e.message}"
|
|
248
|
+
rescue ElevenRb::Errors::APIError => e
|
|
249
|
+
puts "API error: #{e.message} (status: #{e.http_status})"
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Rails Integration
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# config/initializers/eleven_rb.rb
|
|
257
|
+
ElevenRb.configure do |config|
|
|
258
|
+
config.api_key = Rails.application.credentials.dig(:elevenlabs, :api_key)
|
|
259
|
+
|
|
260
|
+
config.on_error = ->(error:, **) {
|
|
261
|
+
Sentry.capture_exception(error, tags: { service: "elevenlabs" })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
config.on_audio_generated = ->(cost_info:, **) {
|
|
265
|
+
TtsUsage.create!(cost_info)
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Then use anywhere
|
|
270
|
+
audio = ElevenRb.client.tts.generate("Hello", voice_id: "abc123")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Voice Slot Limits by Plan
|
|
274
|
+
|
|
275
|
+
| Plan | Voice Slots |
|
|
276
|
+
|------|-------------|
|
|
277
|
+
| Free | 3 |
|
|
278
|
+
| Starter | 10 |
|
|
279
|
+
| Creator | 30 |
|
|
280
|
+
| Pro | 160 |
|
|
281
|
+
| Scale | 660 |
|
|
282
|
+
| Business | 660 |
|
|
283
|
+
|
|
284
|
+
## References
|
|
285
|
+
|
|
286
|
+
- [ElevenLabs API Documentation](https://elevenlabs.io/docs/api-reference)
|
|
287
|
+
- [ElevenLabs Developer Portal](https://try.elevenlabs.io/qyk2j8gumrjz)
|
|
288
|
+
- [Voice Library](https://elevenlabs.io/voice-library)
|
|
289
|
+
|
|
290
|
+
## Changelog
|
|
291
|
+
|
|
292
|
+
For a detailed list of changes for each version of this project, please see the [CHANGELOG](CHANGELOG.md).
|
|
293
|
+
|
|
294
|
+
## Development
|
|
295
|
+
|
|
296
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bundle exec rake console` for an interactive prompt that will allow you to experiment.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
bundle install # Install dependencies
|
|
300
|
+
bundle exec rspec # Run tests
|
|
301
|
+
bundle exec rubocop # Run linter
|
|
302
|
+
bundle exec rake build # Build gem
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Contributing
|
|
306
|
+
|
|
307
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/webventures/eleven_rb.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ElevenRb
|
|
4
|
+
# Provides a callback/hook system for monitoring and extending gem behavior
|
|
5
|
+
#
|
|
6
|
+
# @example Setting up callbacks
|
|
7
|
+
# client = ElevenRb::Client.new(
|
|
8
|
+
# api_key: "...",
|
|
9
|
+
# on_error: ->(error:, method:, path:, context:) {
|
|
10
|
+
# Sentry.capture_exception(error)
|
|
11
|
+
# }
|
|
12
|
+
# )
|
|
13
|
+
module Callbacks
|
|
14
|
+
CALLBACK_NAMES = %i[
|
|
15
|
+
on_request
|
|
16
|
+
on_response
|
|
17
|
+
on_error
|
|
18
|
+
on_audio_generated
|
|
19
|
+
on_retry
|
|
20
|
+
on_rate_limit
|
|
21
|
+
on_voice_added
|
|
22
|
+
on_voice_deleted
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def self.included(base)
|
|
26
|
+
base.attr_accessor(*CALLBACK_NAMES)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Trigger a callback if it's configured
|
|
30
|
+
#
|
|
31
|
+
# @param callback_name [Symbol] the name of the callback
|
|
32
|
+
# @param kwargs [Hash] keyword arguments to pass to the callback
|
|
33
|
+
# @return [Object, nil] the return value of the callback, or nil
|
|
34
|
+
def trigger(callback_name, **kwargs)
|
|
35
|
+
callback = send(callback_name)
|
|
36
|
+
return unless callback.respond_to?(:call)
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
callback.call(**kwargs)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
# Don't let callback errors break the main flow
|
|
42
|
+
warn "[ElevenRb] Callback error in #{callback_name}: #{e.message}"
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ElevenRb
|
|
4
|
+
# Main client for interacting with the ElevenLabs API
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# client = ElevenRb::Client.new(api_key: "your-api-key")
|
|
8
|
+
# audio = client.tts.generate("Hello world", voice_id: "abc123")
|
|
9
|
+
# audio.save_to_file("output.mp3")
|
|
10
|
+
#
|
|
11
|
+
# @example With callbacks
|
|
12
|
+
# client = ElevenRb::Client.new(
|
|
13
|
+
# api_key: "your-api-key",
|
|
14
|
+
# on_error: ->(error:, **) { Sentry.capture_exception(error) },
|
|
15
|
+
# on_audio_generated: ->(cost_info:, **) { track_cost(cost_info) }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @example Voice slot management
|
|
19
|
+
# client.voice_slots.ensure_available(
|
|
20
|
+
# public_user_id: "abc",
|
|
21
|
+
# voice_id: "xyz",
|
|
22
|
+
# name: "Spanish Voice"
|
|
23
|
+
# )
|
|
24
|
+
class Client
|
|
25
|
+
attr_reader :config, :http_client
|
|
26
|
+
|
|
27
|
+
# Initialize a new client
|
|
28
|
+
#
|
|
29
|
+
# @param api_key [String, nil] API key (defaults to ELEVENLABS_API_KEY env var)
|
|
30
|
+
# @param options [Hash] additional configuration options
|
|
31
|
+
# @see Configuration#initialize for all available options
|
|
32
|
+
def initialize(api_key: nil, **options)
|
|
33
|
+
@config = Configuration.new(
|
|
34
|
+
api_key: api_key || ENV.fetch('ELEVENLABS_API_KEY', nil),
|
|
35
|
+
**options
|
|
36
|
+
)
|
|
37
|
+
@config.validate!
|
|
38
|
+
@http_client = HTTP::Client.new(@config)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Voice management resource
|
|
42
|
+
#
|
|
43
|
+
# @return [Resources::Voices]
|
|
44
|
+
def voices
|
|
45
|
+
@voices ||= Resources::Voices.new(http_client)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Text-to-speech resource
|
|
49
|
+
#
|
|
50
|
+
# @return [Resources::TextToSpeech]
|
|
51
|
+
def tts
|
|
52
|
+
@tts ||= Resources::TextToSpeech.new(http_client)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Voice library resource
|
|
56
|
+
#
|
|
57
|
+
# @return [Resources::VoiceLibrary]
|
|
58
|
+
def voice_library
|
|
59
|
+
@voice_library ||= Resources::VoiceLibrary.new(http_client)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Models resource
|
|
63
|
+
#
|
|
64
|
+
# @return [Resources::Models]
|
|
65
|
+
def models
|
|
66
|
+
@models ||= Resources::Models.new(http_client)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# User/account resource
|
|
70
|
+
#
|
|
71
|
+
# @return [Resources::User]
|
|
72
|
+
def user
|
|
73
|
+
@user ||= Resources::User.new(http_client)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Voice slot manager
|
|
77
|
+
#
|
|
78
|
+
# @return [VoiceSlotManager]
|
|
79
|
+
def voice_slots
|
|
80
|
+
@voice_slots ||= VoiceSlotManager.new(self)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Convenience method: generate speech
|
|
84
|
+
#
|
|
85
|
+
# @param text [String] the text to convert
|
|
86
|
+
# @param voice_id [String] the voice ID
|
|
87
|
+
# @param options [Hash] additional options
|
|
88
|
+
# @return [Objects::Audio]
|
|
89
|
+
def generate_speech(text, voice_id:, **options)
|
|
90
|
+
tts.generate(text, voice_id: voice_id, **options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convenience method: stream speech
|
|
94
|
+
#
|
|
95
|
+
# @param text [String] the text to convert
|
|
96
|
+
# @param voice_id [String] the voice ID
|
|
97
|
+
# @param options [Hash] additional options
|
|
98
|
+
# @yield [String] each chunk of audio
|
|
99
|
+
def stream_speech(text, voice_id:, **options, &block)
|
|
100
|
+
tts.stream(text, voice_id: voice_id, **options, &block)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get the TTS adapter for wrapper compatibility
|
|
104
|
+
#
|
|
105
|
+
# @return [TTSAdapter]
|
|
106
|
+
def adapter
|
|
107
|
+
@adapter ||= TTSAdapter.new(self)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ElevenRb
|
|
4
|
+
module Collections
|
|
5
|
+
# Base class for collections of objects
|
|
6
|
+
#
|
|
7
|
+
# Provides Enumerable support and pagination helpers
|
|
8
|
+
class Base
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :items, :raw_response
|
|
12
|
+
|
|
13
|
+
# Create collection from API response
|
|
14
|
+
#
|
|
15
|
+
# @param response [Hash] the API response
|
|
16
|
+
# @return [Base]
|
|
17
|
+
def self.from_response(response)
|
|
18
|
+
new(response)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Initialize collection
|
|
22
|
+
#
|
|
23
|
+
# @param response [Hash] the API response
|
|
24
|
+
def initialize(response)
|
|
25
|
+
@raw_response = response
|
|
26
|
+
@items = parse_items(response)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Iterate over items
|
|
30
|
+
#
|
|
31
|
+
# @yield [Object] each item
|
|
32
|
+
def each(&block)
|
|
33
|
+
items.each(&block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get item by index
|
|
37
|
+
#
|
|
38
|
+
# @param index [Integer]
|
|
39
|
+
# @return [Object, nil]
|
|
40
|
+
def [](index)
|
|
41
|
+
items[index]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get collection size
|
|
45
|
+
#
|
|
46
|
+
# @return [Integer]
|
|
47
|
+
def size
|
|
48
|
+
items.size
|
|
49
|
+
end
|
|
50
|
+
alias length size
|
|
51
|
+
alias count size
|
|
52
|
+
|
|
53
|
+
# Check if collection is empty
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def empty?
|
|
57
|
+
items.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get first item
|
|
61
|
+
#
|
|
62
|
+
# @return [Object, nil]
|
|
63
|
+
def first
|
|
64
|
+
items.first
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get last item
|
|
68
|
+
#
|
|
69
|
+
# @return [Object, nil]
|
|
70
|
+
def last
|
|
71
|
+
items.last
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Convert to array
|
|
75
|
+
#
|
|
76
|
+
# @return [Array]
|
|
77
|
+
def to_a
|
|
78
|
+
items.dup
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Override in subclasses to parse items
|
|
84
|
+
#
|
|
85
|
+
# @param response [Hash] the API response
|
|
86
|
+
# @return [Array]
|
|
87
|
+
def parse_items(response)
|
|
88
|
+
raise NotImplementedError, 'Subclasses must implement #parse_items'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ElevenRb
|
|
4
|
+
module Collections
|
|
5
|
+
# Collection of LibraryVoice objects from the shared voice library
|
|
6
|
+
class LibraryVoiceCollection < Base
|
|
7
|
+
# Check if there are more results
|
|
8
|
+
#
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def has_more?
|
|
11
|
+
raw_response['has_more'] == true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Get the cursor for next page
|
|
15
|
+
#
|
|
16
|
+
# @return [String, nil]
|
|
17
|
+
def next_cursor
|
|
18
|
+
raw_response['last_sort_id']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Find voice by ID
|
|
22
|
+
#
|
|
23
|
+
# @param voice_id [String]
|
|
24
|
+
# @return [Objects::LibraryVoice, nil]
|
|
25
|
+
def find_by_id(voice_id)
|
|
26
|
+
items.find { |v| v.voice_id == voice_id }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Find voice by name (case-insensitive)
|
|
30
|
+
#
|
|
31
|
+
# @param name [String]
|
|
32
|
+
# @return [Objects::LibraryVoice, nil]
|
|
33
|
+
def find_by_name(name)
|
|
34
|
+
items.find { |v| v.name&.downcase == name.downcase }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Filter voices by gender
|
|
38
|
+
#
|
|
39
|
+
# @param gender [String]
|
|
40
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
41
|
+
def by_gender(gender)
|
|
42
|
+
items.select { |v| v.gender&.downcase == gender.downcase }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Filter voices by language
|
|
46
|
+
#
|
|
47
|
+
# @param language [String]
|
|
48
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
49
|
+
def by_language(language)
|
|
50
|
+
items.select { |v| v.language&.downcase == language.downcase }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Filter voices by accent
|
|
54
|
+
#
|
|
55
|
+
# @param accent [String]
|
|
56
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
57
|
+
def by_accent(accent)
|
|
58
|
+
items.select { |v| v.accent&.downcase&.include?(accent.downcase) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get popular voices (high usage)
|
|
62
|
+
#
|
|
63
|
+
# @param threshold [Integer]
|
|
64
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
65
|
+
def popular(threshold: 10_000)
|
|
66
|
+
items.select { |v| v.popular?(threshold: threshold) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get voices available for free users
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
72
|
+
def free_tier
|
|
73
|
+
items.select(&:available_for_free?)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get verified voices only
|
|
77
|
+
#
|
|
78
|
+
# @return [Array<Objects::LibraryVoice>]
|
|
79
|
+
def verified
|
|
80
|
+
items.select(&:verified)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def parse_items(response)
|
|
86
|
+
voices = response['voices'] || []
|
|
87
|
+
voices.map { |v| Objects::LibraryVoice.from_response(v) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|