active_harness 0.3.3 → 0.3.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4297f5db3bd99d90d12679afcc742998ed815606ee25e3c0490187370fad8e2d
4
- data.tar.gz: c2955dc8a5391fcf13fc3752ba490222ea8d55f7127cc90f8aee93937b1bac91
3
+ metadata.gz: 348fea391f00283a45f57b119081ec4c820559e5df34651f988f3d5c68538d03
4
+ data.tar.gz: a5be81902bc6ff447ec64e434a08a25bc9ea193f8d89706cc6e3ec1a7514007e
5
5
  SHA512:
6
- metadata.gz: 731130020cff7ed11514659b2cfc7b9dd6aed0e6a935e9ff198ebf695b44065a7e1a139da1824178b289c1aa13d75e8aaba5b32373b781b4f9639a61d9c5135e
7
- data.tar.gz: 6240251c55447e9a56d3c87d704b441d447ad3c8604b09651c2dfbb9577e20c11165bb4fba740bdf9c0cb386adca2c0ab52a80485c7410d56d1285fddf86ff61
6
+ metadata.gz: 1bd229ceed23615fbb1d59de9d4a8747722eb19849791d9911b218ee474672b6fdf3779f6bb6449af55150b6a8ad99bc5437e106789dc4f85550cc97077c7cca
7
+ data.tar.gz: 6011181b558cfb445f8479e27dc19af4086daa38c994c2d57d61c66c15228638b8a18dee842524c1e590d855b029235eae80a9929e8385874876972d1aea4bb9
@@ -27,7 +27,18 @@ module ActiveHarness
27
27
  # Synchronous — this endpoint has no job/polling API. Unlike OpenRouter's
28
28
  # transcription endpoint, OpenAI's is multipart/form-data only (no
29
29
  # base64/JSON request mode).
30
- def call(model:, audio_data:, audio_format:, language: nil, **_)
30
+ # @param response_format [String] "json" (default, plain text) or,
31
+ # for gpt-4o-transcribe-diarize, "diarized_json" — returns per-segment
32
+ # speaker/text/start/end instead of just plain text.
33
+ # @param chunking_strategy [String, Hash] required by
34
+ # gpt-4o-transcribe-diarize for any audio over 30 seconds; "auto" is
35
+ # the simple/common value, or a voice-activity-detection config hash.
36
+ # @param known_speaker_names [Array<String>] up to 4 labels, paired
37
+ # positionally with known_speaker_references, for gpt-4o-transcribe-diarize.
38
+ # @param known_speaker_references [Array<String>] up to 4 short (2-10s)
39
+ # reference clips as base64 data URLs, e.g. "data:audio/wav;base64,...".
40
+ def call(model:, audio_data:, audio_format:, language: nil, response_format: nil,
41
+ chunking_strategy: nil, known_speaker_names: nil, known_speaker_references: nil, **_)
31
42
  content_type = CONTENT_TYPES[audio_format]
32
43
  unless content_type
33
44
  raise Errors::InvalidRequestError,
@@ -36,9 +47,16 @@ module ActiveHarness
36
47
 
37
48
  boundary = SecureRandom.hex(16)
38
49
  fields = { "model" => model }
39
- fields["language"] = language if language
50
+ fields["language"] = language if language
51
+ fields["response_format"] = response_format if response_format
52
+ fields["chunking_strategy"] = chunking_strategy if chunking_strategy
40
53
 
41
- body = build_multipart_body(boundary, fields, audio_data, "audio.#{audio_format}", content_type)
54
+ array_fields = {
55
+ "known_speaker_names[]" => known_speaker_names,
56
+ "known_speaker_references[]" => known_speaker_references
57
+ }
58
+
59
+ body = build_multipart_body(boundary, fields, audio_data, "audio.#{audio_format}", content_type, array_fields: array_fields)
42
60
  headers = {
43
61
  "Content-Type" => "multipart/form-data; boundary=#{boundary}",
44
62
  "Authorization" => "Bearer #{api_key}"
@@ -48,15 +66,24 @@ module ActiveHarness
48
66
  data = parse!(raw)
49
67
  handle_error!(data)
50
68
 
51
- text = data["text"]
52
- raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if text.nil?
69
+ # diarized_json returns a structured { segments: [...] } shape (each
70
+ # with speaker/text/start/end) — hand the whole thing back as JSON
71
+ # text so `format :json` on the Request subclass parses it into a
72
+ # Hash. Plain "json"/unset keeps the existing behavior: just the
73
+ # flat text.
74
+ content =
75
+ if response_format == "diarized_json"
76
+ data.to_json
77
+ else
78
+ data["text"].tap { |t| raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if t.nil? }
79
+ end
53
80
 
54
- { content: text, provider: :openai, model: model, usage: extract_transcription_usage(data) }
81
+ { content: content, provider: :openai, model: model, usage: extract_transcription_usage(data) }
55
82
  end
56
83
 
57
84
  private
58
85
 
59
- def build_multipart_body(boundary, fields, file_data, filename, content_type)
86
+ def build_multipart_body(boundary, fields, file_data, filename, content_type, array_fields: {})
60
87
  body = +""
61
88
  fields.each do |name, value|
62
89
  body << "--#{boundary}\r\n"
@@ -64,6 +91,14 @@ module ActiveHarness
64
91
  body << "#{value}\r\n"
65
92
  end
66
93
 
94
+ array_fields.each do |name, values|
95
+ Array(values).each do |value|
96
+ body << "--#{boundary}\r\n"
97
+ body << "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n"
98
+ body << "#{value}\r\n"
99
+ end
100
+ end
101
+
67
102
  body << "--#{boundary}\r\n"
68
103
  body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
69
104
  body << "Content-Type: #{content_type}\r\n\r\n"
@@ -13,11 +13,22 @@ module ActiveHarness
13
13
  # @param audio_data [String] raw binary audio bytes
14
14
  # @param audio_format [String] "wav", "mp3", "flac", "m4a", "ogg", "webm", "aac"
15
15
  # @param language [String] ISO-639-1 code, e.g. "en" (optional — auto-detected if omitted)
16
+ # @param response_format [String] "json" (default, plain text) or "verbose_json"
17
+ # — the latter returns per-segment start/end timestamps (and a "speaker"
18
+ # index per segment when the provider supports diarization).
19
+ # @param timestamp_granularities [Array<String>] e.g. ["segment", "word"] —
20
+ # only meaningful with response_format: "verbose_json".
21
+ # @param provider_options [Hash] passed through as-is to the request body's
22
+ # `provider.options` — this is how provider-specific features (e.g. Azure
23
+ # speaker diarization) are enabled: { azure: { diarization: { enabled: true } } }.
24
+ # Only specific provider/model combos support this; most will just ignore
25
+ # an option they don't recognize, but some may 400 on it.
16
26
  #
17
27
  # Synchronous call — OpenRouter's transcription endpoint has no job/polling
18
28
  # API. Upstream providers time out after ~60s per request, so long audio
19
29
  # should be split into shorter chunks by the caller before transcribing.
20
- def call(model:, audio_data:, audio_format:, language: nil, **_)
30
+ def call(model:, audio_data:, audio_format:, language: nil, response_format: nil,
31
+ timestamp_granularities: nil, provider_options: nil, **_)
21
32
  headers = {
22
33
  "Content-Type" => "application/json",
23
34
  "Authorization" => "Bearer #{api_key}"
@@ -29,16 +40,27 @@ module ActiveHarness
29
40
  model: model,
30
41
  input_audio: { data: Base64.strict_encode64(audio_data), format: audio_format }
31
42
  }
32
- body[:language] = language if language
43
+ body[:language] = language if language
44
+ body[:response_format] = response_format if response_format
45
+ body[:timestamp_granularities] = timestamp_granularities if timestamp_granularities
46
+ body[:provider] = { options: provider_options } if provider_options
33
47
 
34
48
  raw = post_json(URI(ENDPOINT), headers: headers, body: body, timeout: 90)
35
49
  data = parse!(raw)
36
50
  handle_error!(data)
37
51
 
38
- text = data["text"]
39
- raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if text.nil?
52
+ # verbose_json returns a richer structure (language/duration/segments with
53
+ # timestamps and, when supported, speaker) — hand the whole thing back as
54
+ # JSON text so `format :json` on the Request subclass parses it into a Hash.
55
+ # Plain "json"/unset keeps the existing behavior: just the flat text.
56
+ content =
57
+ if response_format == "verbose_json"
58
+ data.to_json
59
+ else
60
+ data["text"].tap { |t| raise Errors::ProviderError, "No transcription text in response: #{data.keys}" if t.nil? }
61
+ end
40
62
 
41
- { content: text, provider: :openrouter, model: model, usage: extract_transcription_usage(data) }
63
+ { content: content, provider: :openrouter, model: model, usage: extract_transcription_usage(data) }
42
64
  end
43
65
 
44
66
  private
@@ -134,17 +134,25 @@ module ActiveHarness
134
134
  @models = []
135
135
  end
136
136
 
137
- def use(provider:, model:, temperature: nil, name: nil, size: nil, quality: nil, retry_attempts: nil, retry_delay: nil, questions: nil)
137
+ def use(provider:, model:, temperature: nil, name: nil, size: nil, quality: nil, retry_attempts: nil, retry_delay: nil,
138
+ questions: nil, response_format: nil, timestamp_granularities: nil, provider_options: nil,
139
+ chunking_strategy: nil, known_speaker_names: nil, known_speaker_references: nil)
138
140
  @models << {
139
- provider: provider,
140
- model: model,
141
- temperature: temperature,
142
- name: name,
143
- size: size,
144
- quality: quality,
145
- retry_attempts: retry_attempts,
146
- retry_delay: retry_delay,
147
- questions: questions
141
+ provider: provider,
142
+ model: model,
143
+ temperature: temperature,
144
+ name: name,
145
+ size: size,
146
+ quality: quality,
147
+ retry_attempts: retry_attempts,
148
+ retry_delay: retry_delay,
149
+ questions: questions,
150
+ response_format: response_format,
151
+ timestamp_granularities: timestamp_granularities,
152
+ provider_options: provider_options,
153
+ chunking_strategy: chunking_strategy,
154
+ known_speaker_names: known_speaker_names,
155
+ known_speaker_references: known_speaker_references
148
156
  }.compact
149
157
  end
150
158
 
@@ -100,7 +100,13 @@ module ActiveHarness
100
100
 
101
101
  opts = { model: entry[:model], audio_data: File.binread(path), audio_format: format }
102
102
  language = entry[:language] || @config[:transcribe_language]
103
- opts[:language] = language if language
103
+ opts[:language] = language if language
104
+ opts[:response_format] = entry[:response_format] if entry[:response_format]
105
+ opts[:timestamp_granularities] = entry[:timestamp_granularities] if entry[:timestamp_granularities]
106
+ opts[:provider_options] = entry[:provider_options] if entry[:provider_options]
107
+ opts[:chunking_strategy] = entry[:chunking_strategy] if entry[:chunking_strategy]
108
+ opts[:known_speaker_names] = entry[:known_speaker_names] if entry[:known_speaker_names]
109
+ opts[:known_speaker_references] = entry[:known_speaker_references] if entry[:known_speaker_references]
104
110
  factory.call.call(**opts)
105
111
  end
106
112
 
@@ -140,6 +140,7 @@ module ActiveHarness
140
140
  model: entry[:model],
141
141
  error: error.message,
142
142
  error_code: error.respond_to?(:error_code) ? error.error_code : nil,
143
+ error_metadata: error.respond_to?(:metadata) ? error.metadata : nil,
143
144
  execution_time: elapsed
144
145
  }
145
146
  end
@@ -35,7 +35,7 @@ require_relative "active_harness/pipeline"
35
35
  require_relative "active_harness/railtie" if defined?(Rails::Railtie)
36
36
 
37
37
  module ActiveHarness
38
- VERSION = "0.3.3"
38
+ VERSION = "0.3.5"
39
39
 
40
40
  class << self
41
41
  # Configure ActiveHarness.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-25 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby