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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/lib/zuno/version.rb +1 -1
  4. data/lib/zuno.rb +177 -0
  5. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d6d4d8dd97c7ac8743ddc66545aeda41ed824357965c693ac244fcf46de5df2
4
- data.tar.gz: d7e0a1a6029482af54dbf7bc6e5196b39dd60e424b1fb18b4003543cfbe79867
3
+ metadata.gz: 375f446f6d41ed1c8f05361915316c3da7fbc571ee5ec4d59316739f9fd6a14f
4
+ data.tar.gz: 641e201f64dc3076185e2a6593932e67847f05ce4c5a66866b200b8db4d38241
5
5
  SHA512:
6
- metadata.gz: 4862131a38d657f175bbc488599d5cbcd97899b43bdac7a26117a437c081bfdaa365e4a2e5255e7e48464aa5e5d129e14106364727b19bae4b8166ff779cd621
7
- data.tar.gz: 96340e7d0b4a2153d58b328cbdcbec5836ddd76724941fd8efa39343de14c052a51c2031e916562e0b66de5044832a429f2332c92e4325f40d6eefc2d1e67454
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zuno
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.2"
5
5
  end
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.1
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-03-31 00:00:00.000000000 Z
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