zuno 1.0.0 → 1.0.2
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/zuno/version.rb +1 -1
- data/lib/zuno.rb +318 -5
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 375f446f6d41ed1c8f05361915316c3da7fbc571ee5ec4d59316739f9fd6a14f
|
|
4
|
+
data.tar.gz: 641e201f64dc3076185e2a6593932e67847f05ce4c5a66866b200b8db4d38241
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76d4be87dad8f35fc9e879a14705035344823f43c0ba45c6d8317d5b8e0a273c6349db11294b8e17c5dfb901dec9006fa488b8ebe8633baec33aa66e74be9166
|
|
7
|
+
data.tar.gz: 8555df550de0513330c086ec4b676d584a1908feb983d2ca59d32c8d685f626d2c767960e6e1dde23dbb17ac0cfc903aee251bef71519ffde27f8c1524cd2d68
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.2](https://github.com/dqnamo/zuno/compare/v1.0.1...v1.0.2) (2026-04-01)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Add ElevenLabs Scribe batch transcription via `Zuno.transcribe` and `Zuno.elevenlabs` ([ElevenLabs speech-to-text API](https://elevenlabs.io/docs/api-reference/speech-to-text/convert)).
|
data/lib/zuno/version.rb
CHANGED
data/lib/zuno.rb
CHANGED
|
@@ -76,7 +76,9 @@ module Zuno
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
OPENROUTER_ADAPTER_CONFIG_KEYS = %i[api_key app_url title timeout].freeze
|
|
79
|
+
AI_GATEWAY_ADAPTER_CONFIG_KEYS = %i[api_key timeout base_url].freeze
|
|
79
80
|
REPLICATE_ADAPTER_CONFIG_KEYS = %i[api_key timeout].freeze
|
|
81
|
+
ELEVENLABS_ADAPTER_CONFIG_KEYS = %i[api_key timeout base_url].freeze
|
|
80
82
|
DEFAULT_MAX_ITERATIONS = 1
|
|
81
83
|
REPLICATE_PREFER_WAIT_SECONDS = 60
|
|
82
84
|
REPLICATE_POLL_INTERVAL_SECONDS = 1
|
|
@@ -113,6 +115,30 @@ module Zuno
|
|
|
113
115
|
)
|
|
114
116
|
end
|
|
115
117
|
|
|
118
|
+
def ai_gateway(
|
|
119
|
+
api_key: nil,
|
|
120
|
+
timeout: Providers::AIGateway::DEFAULT_TIMEOUT,
|
|
121
|
+
base_url: Providers::AIGateway::DEFAULT_BASE_URL
|
|
122
|
+
)
|
|
123
|
+
Providers::AIGateway.new(
|
|
124
|
+
api_key: api_key,
|
|
125
|
+
timeout: timeout,
|
|
126
|
+
base_url: base_url
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def elevenlabs(
|
|
131
|
+
api_key: nil,
|
|
132
|
+
timeout: Providers::ElevenLabs::DEFAULT_TIMEOUT,
|
|
133
|
+
base_url: Providers::ElevenLabs::DEFAULT_BASE_URL
|
|
134
|
+
)
|
|
135
|
+
Providers::ElevenLabs.new(
|
|
136
|
+
api_key: api_key,
|
|
137
|
+
timeout: timeout,
|
|
138
|
+
base_url: base_url
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
116
142
|
def tool(name:, description:, input_schema:, &execute)
|
|
117
143
|
raise ToolError, "A block is required for tool '#{name}'" unless block_given?
|
|
118
144
|
|
|
@@ -124,6 +150,46 @@ module Zuno
|
|
|
124
150
|
)
|
|
125
151
|
end
|
|
126
152
|
|
|
153
|
+
def transcribe(
|
|
154
|
+
model_id: "scribe_v2",
|
|
155
|
+
file: nil,
|
|
156
|
+
cloud_storage_url: nil,
|
|
157
|
+
source_url: nil,
|
|
158
|
+
provider_options: {},
|
|
159
|
+
**options
|
|
160
|
+
)
|
|
161
|
+
validate_transcription_input!(
|
|
162
|
+
model_id: model_id,
|
|
163
|
+
file: file,
|
|
164
|
+
cloud_storage_url: cloud_storage_url,
|
|
165
|
+
source_url: source_url
|
|
166
|
+
)
|
|
167
|
+
resolved_provider_options = merge_provider_options({}, provider_options)
|
|
168
|
+
adapter = provider_adapter(:elevenlabs, resolved_provider_options)
|
|
169
|
+
response = adapter.transcribe(
|
|
170
|
+
model_id: model_id,
|
|
171
|
+
file: file,
|
|
172
|
+
cloud_storage_url: cloud_storage_url,
|
|
173
|
+
source_url: source_url,
|
|
174
|
+
options: options
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
result = {
|
|
178
|
+
text: response["text"],
|
|
179
|
+
language_code: response["language_code"],
|
|
180
|
+
language_probability: response["language_probability"],
|
|
181
|
+
words: response["words"],
|
|
182
|
+
transcripts: response["transcripts"],
|
|
183
|
+
transcription_id: response["transcription_id"],
|
|
184
|
+
raw_response: response
|
|
185
|
+
}
|
|
186
|
+
result.reject { |_key, value| value.nil? }
|
|
187
|
+
rescue ProviderError
|
|
188
|
+
raise
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
raise Error, e.message
|
|
191
|
+
end
|
|
192
|
+
|
|
127
193
|
def generate(
|
|
128
194
|
model:,
|
|
129
195
|
messages: nil,
|
|
@@ -176,7 +242,7 @@ module Zuno
|
|
|
176
242
|
|
|
177
243
|
result =
|
|
178
244
|
case provider
|
|
179
|
-
when :openrouter
|
|
245
|
+
when :openrouter, :ai_gateway
|
|
180
246
|
adapter = provider_adapter(provider, resolved_provider_options)
|
|
181
247
|
generate_openrouter_single(
|
|
182
248
|
model_descriptor: model_descriptor,
|
|
@@ -246,7 +312,9 @@ module Zuno
|
|
|
246
312
|
)
|
|
247
313
|
callback_control = nil
|
|
248
314
|
model_descriptor = normalize_model(model)
|
|
249
|
-
|
|
315
|
+
unless %i[openrouter ai_gateway].include?(model_descriptor.provider.to_sym)
|
|
316
|
+
raise Error, "loop only supports openrouter or ai_gateway provider"
|
|
317
|
+
end
|
|
250
318
|
|
|
251
319
|
resolved_provider_options = merge_provider_options(
|
|
252
320
|
model_descriptor.provider_options,
|
|
@@ -320,6 +388,7 @@ module Zuno
|
|
|
320
388
|
|
|
321
389
|
payload = build_payload(
|
|
322
390
|
model_id: model_descriptor.id,
|
|
391
|
+
provider: model_descriptor.provider,
|
|
323
392
|
messages: llm_messages,
|
|
324
393
|
tools: tool_map,
|
|
325
394
|
tool_choice: resolved_tool_choice,
|
|
@@ -497,7 +566,9 @@ module Zuno
|
|
|
497
566
|
raise ArgumentError, "stream requires a block callback" unless block_given?
|
|
498
567
|
|
|
499
568
|
model_descriptor = normalize_model(model)
|
|
500
|
-
|
|
569
|
+
unless %i[openrouter ai_gateway].include?(model_descriptor.provider.to_sym)
|
|
570
|
+
raise ProviderError, "stream only supports openrouter or ai_gateway provider"
|
|
571
|
+
end
|
|
501
572
|
|
|
502
573
|
resolved_provider_options = merge_provider_options(
|
|
503
574
|
model_descriptor.provider_options,
|
|
@@ -508,6 +579,7 @@ module Zuno
|
|
|
508
579
|
|
|
509
580
|
payload = build_payload(
|
|
510
581
|
model_id: model_descriptor.id,
|
|
582
|
+
provider: model_descriptor.provider,
|
|
511
583
|
messages: llm_messages,
|
|
512
584
|
tools: {},
|
|
513
585
|
tool_choice: nil,
|
|
@@ -608,6 +680,7 @@ module Zuno
|
|
|
608
680
|
|
|
609
681
|
payload = build_payload(
|
|
610
682
|
model_id: model_descriptor.id,
|
|
683
|
+
provider: model_descriptor.provider,
|
|
611
684
|
messages: llm_messages,
|
|
612
685
|
tools: tool_map,
|
|
613
686
|
tool_choice: resolved_tool_choice,
|
|
@@ -743,6 +816,25 @@ module Zuno
|
|
|
743
816
|
end
|
|
744
817
|
private_class_method :validate_no_webhook_support!
|
|
745
818
|
|
|
819
|
+
def validate_transcription_input!(model_id:, file:, cloud_storage_url:, source_url:)
|
|
820
|
+
model_id_value = model_id.to_s.strip
|
|
821
|
+
raise Error, "model_id is required" if model_id_value.empty?
|
|
822
|
+
|
|
823
|
+
inputs = []
|
|
824
|
+
inputs << :file unless file.nil?
|
|
825
|
+
inputs << :cloud_storage_url unless cloud_storage_url.nil? || cloud_storage_url.to_s.strip.empty?
|
|
826
|
+
inputs << :source_url unless source_url.nil? || source_url.to_s.strip.empty?
|
|
827
|
+
|
|
828
|
+
if inputs.empty?
|
|
829
|
+
raise Error, "transcribe requires one input: file, cloud_storage_url, or source_url"
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
return if inputs.length == 1
|
|
833
|
+
|
|
834
|
+
raise Error, "transcribe accepts exactly one input: file, cloud_storage_url, or source_url"
|
|
835
|
+
end
|
|
836
|
+
private_class_method :validate_transcription_input!
|
|
837
|
+
|
|
746
838
|
def normalize_tools(tools)
|
|
747
839
|
return {} if tools.nil?
|
|
748
840
|
|
|
@@ -795,7 +887,7 @@ module Zuno
|
|
|
795
887
|
end
|
|
796
888
|
private_class_method :normalize_messages
|
|
797
889
|
|
|
798
|
-
def build_payload(model_id:, messages:, tools:, tool_choice:, temperature:, max_tokens:, provider_options:)
|
|
890
|
+
def build_payload(model_id:, provider:, messages:, tools:, tool_choice:, temperature:, max_tokens:, provider_options:)
|
|
799
891
|
payload = {
|
|
800
892
|
"model" => model_id,
|
|
801
893
|
"messages" => messages
|
|
@@ -806,12 +898,28 @@ module Zuno
|
|
|
806
898
|
payload["tools"] = tools.values.map(&:as_provider_tool) unless tools.empty?
|
|
807
899
|
payload["tool_choice"] = deep_stringify(tool_choice) unless tool_choice.nil?
|
|
808
900
|
|
|
809
|
-
request_options = reject_keys(provider_options,
|
|
901
|
+
request_options = reject_keys(provider_options, provider_adapter_config_keys(provider) + [ :tool_choice ])
|
|
810
902
|
payload.merge!(deep_stringify(request_options)) if request_options.is_a?(Hash)
|
|
811
903
|
payload
|
|
812
904
|
end
|
|
813
905
|
private_class_method :build_payload
|
|
814
906
|
|
|
907
|
+
def provider_adapter_config_keys(provider)
|
|
908
|
+
case provider.to_sym
|
|
909
|
+
when :openrouter
|
|
910
|
+
OPENROUTER_ADAPTER_CONFIG_KEYS
|
|
911
|
+
when :ai_gateway
|
|
912
|
+
AI_GATEWAY_ADAPTER_CONFIG_KEYS
|
|
913
|
+
when :replicate
|
|
914
|
+
REPLICATE_ADAPTER_CONFIG_KEYS
|
|
915
|
+
when :elevenlabs
|
|
916
|
+
ELEVENLABS_ADAPTER_CONFIG_KEYS
|
|
917
|
+
else
|
|
918
|
+
[]
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
private_class_method :provider_adapter_config_keys
|
|
922
|
+
|
|
815
923
|
def merge_provider_options(model_provider_options, call_provider_options)
|
|
816
924
|
merged = {}
|
|
817
925
|
merged.merge!(default_provider_options) if default_provider_options.is_a?(Hash)
|
|
@@ -826,9 +934,15 @@ module Zuno
|
|
|
826
934
|
when :openrouter
|
|
827
935
|
config = pick_keys(provider_options, OPENROUTER_ADAPTER_CONFIG_KEYS)
|
|
828
936
|
Providers::OpenRouter.new(**config)
|
|
937
|
+
when :ai_gateway
|
|
938
|
+
config = pick_keys(provider_options, AI_GATEWAY_ADAPTER_CONFIG_KEYS)
|
|
939
|
+
Providers::AIGateway.new(**config)
|
|
829
940
|
when :replicate
|
|
830
941
|
config = pick_keys(provider_options, REPLICATE_ADAPTER_CONFIG_KEYS)
|
|
831
942
|
Providers::Replicate.new(**config)
|
|
943
|
+
when :elevenlabs
|
|
944
|
+
config = pick_keys(provider_options, ELEVENLABS_ADAPTER_CONFIG_KEYS)
|
|
945
|
+
Providers::ElevenLabs.new(**config)
|
|
832
946
|
else
|
|
833
947
|
raise ProviderError, "Unsupported provider: #{provider}"
|
|
834
948
|
end
|
|
@@ -1243,6 +1357,105 @@ module Zuno
|
|
|
1243
1357
|
|
|
1244
1358
|
end
|
|
1245
1359
|
|
|
1360
|
+
class AIGateway
|
|
1361
|
+
DEFAULT_BASE_URL = "https://ai-gateway.vercel.sh/v1".freeze
|
|
1362
|
+
DEFAULT_TIMEOUT = 120_000
|
|
1363
|
+
|
|
1364
|
+
def initialize(api_key: nil, timeout: DEFAULT_TIMEOUT, base_url: DEFAULT_BASE_URL)
|
|
1365
|
+
@api_key = api_key
|
|
1366
|
+
raise ProviderError, "Vercel Gateway API key not configured" if @api_key.nil? || @api_key.to_s.empty?
|
|
1367
|
+
|
|
1368
|
+
@timeout = timeout
|
|
1369
|
+
@base_url = base_url.to_s.empty? ? DEFAULT_BASE_URL : base_url.to_s
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def model(model_id)
|
|
1373
|
+
ModelDescriptor.new(
|
|
1374
|
+
id: model_id,
|
|
1375
|
+
provider: :ai_gateway,
|
|
1376
|
+
provider_options: provider_options
|
|
1377
|
+
)
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
def chat(payload)
|
|
1381
|
+
response = Typhoeus.post(
|
|
1382
|
+
chat_completions_url,
|
|
1383
|
+
headers: headers,
|
|
1384
|
+
body: JSON.generate(payload),
|
|
1385
|
+
timeout: @timeout
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
validate_response!(response)
|
|
1389
|
+
parsed = JSON.parse(response.body)
|
|
1390
|
+
raise ProviderError, "Vercel Gateway returned invalid JSON" unless parsed.is_a?(Hash)
|
|
1391
|
+
|
|
1392
|
+
parsed
|
|
1393
|
+
rescue JSON::ParserError => e
|
|
1394
|
+
raise ProviderError, "Failed to parse Vercel Gateway response: #{e.message}"
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
def stream(payload)
|
|
1398
|
+
raise ArgumentError, "stream requires a block callback" unless block_given?
|
|
1399
|
+
|
|
1400
|
+
request = Typhoeus::Request.new(
|
|
1401
|
+
chat_completions_url,
|
|
1402
|
+
method: :post,
|
|
1403
|
+
headers: headers,
|
|
1404
|
+
body: JSON.generate(payload),
|
|
1405
|
+
timeout: @timeout
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
parser = SseParser.new { |data| yield(data) }
|
|
1409
|
+
request.on_body do |chunk|
|
|
1410
|
+
parser.push(chunk)
|
|
1411
|
+
nil
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
request.run
|
|
1415
|
+
validate_response!(request.response)
|
|
1416
|
+
parser.flush
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
private
|
|
1420
|
+
|
|
1421
|
+
def provider_options
|
|
1422
|
+
{
|
|
1423
|
+
api_key: @api_key,
|
|
1424
|
+
timeout: @timeout,
|
|
1425
|
+
base_url: @base_url
|
|
1426
|
+
}
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
def chat_completions_url
|
|
1430
|
+
"#{@base_url}/chat/completions"
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
def headers
|
|
1434
|
+
{
|
|
1435
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
1436
|
+
"Content-Type" => "application/json"
|
|
1437
|
+
}
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
def validate_response!(response)
|
|
1441
|
+
raise ProviderError, "No response returned from Vercel Gateway" if response.nil?
|
|
1442
|
+
raise ProviderError, "Vercel Gateway request timed out" if response.timed_out?
|
|
1443
|
+
|
|
1444
|
+
status = response.code.to_i
|
|
1445
|
+
body = response.body.to_s
|
|
1446
|
+
message = body.length > 300 ? "#{body[0, 300]}..." : body
|
|
1447
|
+
|
|
1448
|
+
return if status >= 200 && status < 300
|
|
1449
|
+
|
|
1450
|
+
if status.positive?
|
|
1451
|
+
raise ProviderError, "Vercel Gateway responded with HTTP #{status}: #{message}"
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
suffix = message.empty? ? "" : ": #{message}"
|
|
1455
|
+
raise ProviderError, "Vercel Gateway request failed: #{response.return_code}#{suffix}"
|
|
1456
|
+
end
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1246
1459
|
class Replicate
|
|
1247
1460
|
API_BASE_URL = "https://api.replicate.com/v1".freeze
|
|
1248
1461
|
DEFAULT_TIMEOUT = 120_000
|
|
@@ -1370,6 +1583,106 @@ module Zuno
|
|
|
1370
1583
|
raise ProviderError, "Replicate request failed: #{response.return_code}#{suffix}"
|
|
1371
1584
|
end
|
|
1372
1585
|
end
|
|
1586
|
+
|
|
1587
|
+
class ElevenLabs
|
|
1588
|
+
DEFAULT_BASE_URL = "https://api.elevenlabs.io".freeze
|
|
1589
|
+
SPEECH_TO_TEXT_PATH = "/v1/speech-to-text".freeze
|
|
1590
|
+
DEFAULT_TIMEOUT = 120_000
|
|
1591
|
+
|
|
1592
|
+
def initialize(api_key: nil, timeout: DEFAULT_TIMEOUT, base_url: DEFAULT_BASE_URL)
|
|
1593
|
+
@api_key = api_key
|
|
1594
|
+
raise ProviderError, "ElevenLabs API key not configured" if @api_key.nil? || @api_key.to_s.empty?
|
|
1595
|
+
|
|
1596
|
+
@timeout = timeout
|
|
1597
|
+
@base_url = base_url.to_s.empty? ? DEFAULT_BASE_URL : base_url.to_s
|
|
1598
|
+
end
|
|
1599
|
+
|
|
1600
|
+
def transcribe(model_id:, file:, cloud_storage_url:, source_url:, options:)
|
|
1601
|
+
opened_file = nil
|
|
1602
|
+
body = build_body(
|
|
1603
|
+
model_id: model_id,
|
|
1604
|
+
file: file,
|
|
1605
|
+
cloud_storage_url: cloud_storage_url,
|
|
1606
|
+
source_url: source_url,
|
|
1607
|
+
options: options
|
|
1608
|
+
) do |candidate|
|
|
1609
|
+
opened_file = candidate
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
response = Typhoeus.post(
|
|
1613
|
+
speech_to_text_url,
|
|
1614
|
+
headers: headers,
|
|
1615
|
+
body: body,
|
|
1616
|
+
multipart: true,
|
|
1617
|
+
timeout: @timeout
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
validate_response!(response)
|
|
1621
|
+
parsed = JSON.parse(response.body)
|
|
1622
|
+
raise ProviderError, "ElevenLabs returned invalid JSON" unless parsed.is_a?(Hash)
|
|
1623
|
+
|
|
1624
|
+
parsed
|
|
1625
|
+
rescue JSON::ParserError => e
|
|
1626
|
+
raise ProviderError, "Failed to parse ElevenLabs response: #{e.message}"
|
|
1627
|
+
ensure
|
|
1628
|
+
opened_file.close if opened_file.is_a?(File) && !opened_file.closed?
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
private
|
|
1632
|
+
|
|
1633
|
+
def speech_to_text_url
|
|
1634
|
+
"#{@base_url}#{SPEECH_TO_TEXT_PATH}"
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
def headers
|
|
1638
|
+
{ "xi-api-key" => @api_key }
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
def build_body(model_id:, file:, cloud_storage_url:, source_url:, options:)
|
|
1642
|
+
body = {
|
|
1643
|
+
"model_id" => model_id.to_s
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
normalized_options = options.is_a?(Hash) ? options : {}
|
|
1647
|
+
normalized_options.each do |key, value|
|
|
1648
|
+
next if value.nil?
|
|
1649
|
+
|
|
1650
|
+
body[key.to_s] = value
|
|
1651
|
+
end
|
|
1652
|
+
|
|
1653
|
+
if file.is_a?(String)
|
|
1654
|
+
opened_file = File.open(file, "rb")
|
|
1655
|
+
body["file"] = opened_file
|
|
1656
|
+
yield(opened_file) if block_given?
|
|
1657
|
+
elsif !file.nil?
|
|
1658
|
+
body["file"] = file
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
body["cloud_storage_url"] = cloud_storage_url.to_s unless cloud_storage_url.nil? || cloud_storage_url.to_s.strip.empty?
|
|
1662
|
+
body["source_url"] = source_url.to_s unless source_url.nil? || source_url.to_s.strip.empty?
|
|
1663
|
+
body
|
|
1664
|
+
rescue Errno::ENOENT => e
|
|
1665
|
+
raise ProviderError, "Failed to open transcription file: #{e.message}"
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
def validate_response!(response)
|
|
1669
|
+
raise ProviderError, "No response returned from ElevenLabs" if response.nil?
|
|
1670
|
+
raise ProviderError, "ElevenLabs request timed out" if response.timed_out?
|
|
1671
|
+
|
|
1672
|
+
status = response.code.to_i
|
|
1673
|
+
body = response.body.to_s
|
|
1674
|
+
message = body.length > 300 ? "#{body[0, 300]}..." : body
|
|
1675
|
+
|
|
1676
|
+
return if status >= 200 && status < 300
|
|
1677
|
+
|
|
1678
|
+
if status.positive?
|
|
1679
|
+
raise ProviderError, "ElevenLabs responded with HTTP #{status}: #{message}"
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
suffix = message.empty? ? "" : ": #{message}"
|
|
1683
|
+
raise ProviderError, "ElevenLabs request failed: #{response.return_code}#{suffix}"
|
|
1684
|
+
end
|
|
1685
|
+
end
|
|
1373
1686
|
end
|
|
1374
1687
|
|
|
1375
1688
|
class SseParser
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zuno
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hyperaide
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: typhoeus
|
|
@@ -53,13 +53,14 @@ dependencies:
|
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '3.13'
|
|
55
55
|
description: Standalone Ruby SDK for AI generation across OpenRouter and Replicate,
|
|
56
|
-
with iterative tool loops and SSE streaming.
|
|
56
|
+
ElevenLabs speech-to-text, with iterative tool loops and SSE streaming.
|
|
57
57
|
email:
|
|
58
58
|
- team@hyperaide.dev
|
|
59
59
|
executables: []
|
|
60
60
|
extensions: []
|
|
61
61
|
extra_rdoc_files: []
|
|
62
62
|
files:
|
|
63
|
+
- CHANGELOG.md
|
|
63
64
|
- README.md
|
|
64
65
|
- lib/zuno.rb
|
|
65
66
|
- lib/zuno/version.rb
|