zuno 1.0.1 → 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 +177 -0
- 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
|
@@ -78,6 +78,7 @@ module Zuno
|
|
|
78
78
|
OPENROUTER_ADAPTER_CONFIG_KEYS = %i[api_key app_url title timeout].freeze
|
|
79
79
|
AI_GATEWAY_ADAPTER_CONFIG_KEYS = %i[api_key timeout base_url].freeze
|
|
80
80
|
REPLICATE_ADAPTER_CONFIG_KEYS = %i[api_key timeout].freeze
|
|
81
|
+
ELEVENLABS_ADAPTER_CONFIG_KEYS = %i[api_key timeout base_url].freeze
|
|
81
82
|
DEFAULT_MAX_ITERATIONS = 1
|
|
82
83
|
REPLICATE_PREFER_WAIT_SECONDS = 60
|
|
83
84
|
REPLICATE_POLL_INTERVAL_SECONDS = 1
|
|
@@ -126,6 +127,18 @@ module Zuno
|
|
|
126
127
|
)
|
|
127
128
|
end
|
|
128
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
|
+
|
|
129
142
|
def tool(name:, description:, input_schema:, &execute)
|
|
130
143
|
raise ToolError, "A block is required for tool '#{name}'" unless block_given?
|
|
131
144
|
|
|
@@ -137,6 +150,46 @@ module Zuno
|
|
|
137
150
|
)
|
|
138
151
|
end
|
|
139
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
|
+
|
|
140
193
|
def generate(
|
|
141
194
|
model:,
|
|
142
195
|
messages: nil,
|
|
@@ -763,6 +816,25 @@ module Zuno
|
|
|
763
816
|
end
|
|
764
817
|
private_class_method :validate_no_webhook_support!
|
|
765
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
|
+
|
|
766
838
|
def normalize_tools(tools)
|
|
767
839
|
return {} if tools.nil?
|
|
768
840
|
|
|
@@ -840,6 +912,8 @@ module Zuno
|
|
|
840
912
|
AI_GATEWAY_ADAPTER_CONFIG_KEYS
|
|
841
913
|
when :replicate
|
|
842
914
|
REPLICATE_ADAPTER_CONFIG_KEYS
|
|
915
|
+
when :elevenlabs
|
|
916
|
+
ELEVENLABS_ADAPTER_CONFIG_KEYS
|
|
843
917
|
else
|
|
844
918
|
[]
|
|
845
919
|
end
|
|
@@ -866,6 +940,9 @@ module Zuno
|
|
|
866
940
|
when :replicate
|
|
867
941
|
config = pick_keys(provider_options, REPLICATE_ADAPTER_CONFIG_KEYS)
|
|
868
942
|
Providers::Replicate.new(**config)
|
|
943
|
+
when :elevenlabs
|
|
944
|
+
config = pick_keys(provider_options, ELEVENLABS_ADAPTER_CONFIG_KEYS)
|
|
945
|
+
Providers::ElevenLabs.new(**config)
|
|
869
946
|
else
|
|
870
947
|
raise ProviderError, "Unsupported provider: #{provider}"
|
|
871
948
|
end
|
|
@@ -1506,6 +1583,106 @@ module Zuno
|
|
|
1506
1583
|
raise ProviderError, "Replicate request failed: #{response.return_code}#{suffix}"
|
|
1507
1584
|
end
|
|
1508
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
|
|
1509
1686
|
end
|
|
1510
1687
|
|
|
1511
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
|