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 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
+ [![Gem Version](https://badge.fury.io/rb/eleven_rb.svg)](https://badge.fury.io/rb/eleven_rb)
4
+ [![CI](https://github.com/webventures/eleven_rb/actions/workflows/ci.yml/badge.svg)](https://github.com/webventures/eleven_rb/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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