active_harness 0.3.4 → 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 +4 -4
- data/lib/active_harness/providers/audio/openai.rb +42 -7
- data/lib/active_harness/providers/audio/openrouter.rb +27 -5
- data/lib/active_harness/request/models.rb +18 -10
- data/lib/active_harness/request/providers.rb +7 -1
- data/lib/active_harness/request.rb +1 -0
- data/lib/active_harness.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 348fea391f00283a45f57b119081ec4c820559e5df34651f988f3d5c68538d03
|
|
4
|
+
data.tar.gz: a5be81902bc6ff447ec64e434a08a25bc9ea193f8d89706cc6e3ec1a7514007e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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"]
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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:
|
|
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]
|
|
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
|
-
|
|
39
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
140
|
-
model:
|
|
141
|
-
temperature:
|
|
142
|
-
name:
|
|
143
|
-
size:
|
|
144
|
-
quality:
|
|
145
|
-
retry_attempts:
|
|
146
|
-
retry_delay:
|
|
147
|
-
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]
|
|
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
|
data/lib/active_harness.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|