transcriptdock 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +65 -0
- data/lib/transcriptdock.rb +130 -0
- metadata +50 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a09a930e042d046cad520ea28c34cfa11be1420fea7781f1f34f24d0e4c728ae
|
|
4
|
+
data.tar.gz: 4902cb250c049366cbf60f284a172a6dceaee2f0978bda5cf3d37585db07a5b8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fbfd77ef2cac8034add09db092c66b96166c2a3ec566cb8e98c0f0260bf61f87a739eb0723c6425f975d7bae47692e8749e7262d9e0038793dbe9b48c84d3130
|
|
7
|
+
data.tar.gz: 42b9466688bb81cf72f1097f297c7965ab01e28511f66573ca60bcf6e66edb4e3302c39a0cc52ead64900a5696fce2273709eae98eb5def0eb77b84d8e85a937
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Transcript Dock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Transcript Dock for Ruby
|
|
2
|
+
|
|
3
|
+
A synchronous client using Ruby's standard libraries. Requires Ruby 3.2 or
|
|
4
|
+
newer. Version 0.1.0.
|
|
5
|
+
|
|
6
|
+
Install:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
gem install transcriptdock
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require 'transcriptdock'
|
|
14
|
+
require 'securerandom'
|
|
15
|
+
|
|
16
|
+
client = TranscriptDock::Client.new(ENV.fetch('TRANSCRIPTDOCK_API_KEY'))
|
|
17
|
+
# Generate once per intended operation; save with the returned job ID.
|
|
18
|
+
key = SecureRandom.uuid
|
|
19
|
+
job = client.create_job(
|
|
20
|
+
{source: {url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'},
|
|
21
|
+
mode: 'captions_only', max_credits: 1},
|
|
22
|
+
idempotency_key: key
|
|
23
|
+
)
|
|
24
|
+
job = client.wait_for_job(job['id'], timeout: 120)
|
|
25
|
+
if job['status'] == 'succeeded'
|
|
26
|
+
puts client.export_transcript(job['result_id'], format: 'srt')
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Creating a job can spend credits. This example requests captions only with a
|
|
31
|
+
one credit ceiling. It does not enable AI fallback. Save the operation key and
|
|
32
|
+
reuse the exact body and same key to recover a lost response. There are no
|
|
33
|
+
automatic retries, including in the underlying HTTP transport.
|
|
34
|
+
|
|
35
|
+
`capabilities`, `create_job`, `get_job`, `get_transcript`, `export_transcript`
|
|
36
|
+
and `wait_for_job` provide the core flow. All public endpoints are available
|
|
37
|
+
through `request(method, path, body: nil, query: {}, idempotency_key: nil)`.
|
|
38
|
+
Use the exact fields in the [API contract](https://www.transcriptdock.com/docs/openapi.yaml)
|
|
39
|
+
for batches, discovery, usage, upload slots and webhook endpoints.
|
|
40
|
+
|
|
41
|
+
Responses are hashes retaining future fields and original timing units.
|
|
42
|
+
Exports return the raw JSON, TXT, SRT or VTT string. Polling returns failed and
|
|
43
|
+
cancelled terminal jobs normally; inspect status first. `JobTimeoutError#job`
|
|
44
|
+
retains the last observed response. `ApiError` exposes `code`, `status`,
|
|
45
|
+
`retryable`, `request_id` and `retry_after_seconds`. Error messages redact bearer
|
|
46
|
+
keys and do not expose raw gateway pages.
|
|
47
|
+
|
|
48
|
+
The default per request timeout is 30 seconds. The constructor accepts
|
|
49
|
+
`base_url:` and `timeout:`. Custom origins require HTTPS, except loopback HTTP
|
|
50
|
+
for development. Redirects are not followed. Upload signed storage URLs
|
|
51
|
+
separately without the API key. Store webhook secrets privately and implement
|
|
52
|
+
callback signature checks following the API documentation.
|
|
53
|
+
|
|
54
|
+
## Check and build
|
|
55
|
+
|
|
56
|
+
From the application repository root with Ruby and Python 3 installed:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
python3 .ci/check.py
|
|
60
|
+
mkdir -p dist
|
|
61
|
+
gem build transcriptdock.gemspec --output dist/transcriptdock-0.1.0.gem
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The shared fixture uses loopback HTTP and spends no credits. No lockfile is
|
|
65
|
+
needed for third party dependencies because this library has none. Release workflow: https://github.com/TranscriptDock/transcriptdock-ruby/actions.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'ipaddr'
|
|
6
|
+
|
|
7
|
+
module TranscriptDock
|
|
8
|
+
VERSION = '0.1.0'
|
|
9
|
+
|
|
10
|
+
class ApiError < StandardError
|
|
11
|
+
attr_reader :code, :status, :retryable, :request_id, :retry_after_seconds
|
|
12
|
+
def initialize(code, message, status: nil, retryable: false, request_id: nil, retry_after_seconds: nil)
|
|
13
|
+
super(message)
|
|
14
|
+
@code, @status, @retryable = code, status, retryable
|
|
15
|
+
@request_id, @retry_after_seconds = request_id, retry_after_seconds
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class JobTimeoutError < StandardError
|
|
20
|
+
attr_reader :job
|
|
21
|
+
def initialize(job)
|
|
22
|
+
super('The job is still running. Check it again using its ID.')
|
|
23
|
+
@job = job
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Client
|
|
28
|
+
def initialize(api_key, base_url: 'https://www.transcriptdock.com', timeout: 30)
|
|
29
|
+
raise ArgumentError, 'Provide an API key without whitespace or control characters.' unless api_key.is_a?(String) && api_key.match?(/\A[!-~]+\z/)
|
|
30
|
+
origin = URI.parse(base_url)
|
|
31
|
+
begin
|
|
32
|
+
loopback = origin.hostname == 'localhost' || IPAddr.new(origin.hostname).loopback?
|
|
33
|
+
rescue IPAddr::InvalidAddressError, ArgumentError
|
|
34
|
+
loopback = false
|
|
35
|
+
end
|
|
36
|
+
unless origin.is_a?(URI::HTTP) && origin.hostname && (origin.scheme == 'https' || (origin.scheme == 'http' && loopback)) && origin.userinfo.nil? && origin.query.nil? && origin.fragment.nil? && ['', '/'].include?(origin.path)
|
|
37
|
+
raise ArgumentError, 'Use an HTTPS origin, or loopback HTTP, without credentials, path, query or fragment.'
|
|
38
|
+
end
|
|
39
|
+
@api_key, @base_url = api_key, base_url.sub(%r{/\z}, '')
|
|
40
|
+
@timeout = positive(timeout, 'timeout')
|
|
41
|
+
rescue URI::InvalidURIError
|
|
42
|
+
raise ArgumentError, 'Provide a valid API origin.'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def request(method, path, body: nil, query: {}, idempotency_key: nil, text: false, timeout: @timeout)
|
|
46
|
+
raise ArgumentError, 'Provide a supported HTTP method.' unless method.is_a?(String) && %w[GET POST PUT PATCH DELETE HEAD OPTIONS].include?(method.upcase)
|
|
47
|
+
method = method.upcase
|
|
48
|
+
unless path.is_a?(String) && path.start_with?('/v1/') && !path.match?(/[?#\\]/) && !URI::DEFAULT_PARSER.unescape(path).split('/').any? { |s| ['.', '..'].include?(s) }
|
|
49
|
+
raise ArgumentError, 'Use a /v1/ path and pass query parameters separately.'
|
|
50
|
+
end
|
|
51
|
+
needs_key = method == 'POST' && (%w[/v1/jobs /v1/batches /v1/video-discoveries /v1/language-discoveries].include?(path) || path.end_with?('/retry'))
|
|
52
|
+
if needs_key || !idempotency_key.nil?
|
|
53
|
+
raise ArgumentError, 'Idempotency keys need 8 to 128 printable ASCII characters.' unless idempotency_key.is_a?(String) && idempotency_key.match?(/\A[ -~]{8,128}\z/)
|
|
54
|
+
end
|
|
55
|
+
uri = URI.parse(@base_url + path)
|
|
56
|
+
uri.query = URI.encode_www_form(query.reject { |_, v| v.nil? }) unless query.empty?
|
|
57
|
+
headers = {'Authorization' => "Bearer #{@api_key}", 'Accept' => 'application/json', 'User-Agent' => "transcriptdock-ruby/#{VERSION}"}
|
|
58
|
+
headers['Idempotency-Key'] = idempotency_key if idempotency_key
|
|
59
|
+
headers['Content-Type'] = 'application/json' unless body.nil?
|
|
60
|
+
req = Net::HTTPGenericRequest.new(method, !body.nil?, method != 'HEAD', uri.request_uri, headers)
|
|
61
|
+
req.body = JSON.generate(body) unless body.nil?
|
|
62
|
+
http = Net::HTTP.new(uri.hostname, uri.port, nil)
|
|
63
|
+
http.use_ssl = uri.scheme == 'https'
|
|
64
|
+
http.max_retries = 0
|
|
65
|
+
http.open_timeout = http.read_timeout = http.write_timeout = positive(timeout, 'timeout')
|
|
66
|
+
begin
|
|
67
|
+
response = http.start { |connection| connection.request(req) }
|
|
68
|
+
rescue IOError, SystemCallError, Timeout::Error, SocketError, OpenSSL::SSL::SSLError
|
|
69
|
+
raise ApiError.new('NETWORK_ERROR', 'Could not reach the API. Check the job before retrying a submission.', retryable: true), cause: nil
|
|
70
|
+
end
|
|
71
|
+
status = response.code.to_i
|
|
72
|
+
if status < 200 || status >= 300
|
|
73
|
+
begin
|
|
74
|
+
envelope = JSON.parse(response.body)
|
|
75
|
+
rescue JSON::ParserError
|
|
76
|
+
envelope = {}
|
|
77
|
+
end
|
|
78
|
+
envelope = {} unless envelope.is_a?(Hash)
|
|
79
|
+
error = envelope['error'].is_a?(Hash) ? envelope['error'] : {}
|
|
80
|
+
raise ApiError.new(redact(error['code'] || 'HTTP_ERROR'), redact(error['message'] || "Request failed with status #{status}."), status: status,
|
|
81
|
+
retryable: error.fetch('retryable', status == 429 || status >= 500), request_id: envelope['request_id'] && redact(envelope['request_id']), retry_after_seconds: error['retry_after_seconds'])
|
|
82
|
+
end
|
|
83
|
+
return response.body if text
|
|
84
|
+
begin
|
|
85
|
+
result = JSON.parse(response.body)
|
|
86
|
+
rescue JSON::ParserError
|
|
87
|
+
raise ApiError.new('INVALID_RESPONSE', 'The API did not return JSON.'), cause: nil
|
|
88
|
+
end
|
|
89
|
+
raise ApiError.new('INVALID_RESPONSE', 'The API did not return a JSON object.') unless result.is_a?(Hash)
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def capabilities = request('GET', '/v1/capabilities')
|
|
94
|
+
def create_job(body, idempotency_key:) = request('POST', '/v1/jobs', body: body, idempotency_key: idempotency_key)
|
|
95
|
+
def get_job(id) = request('GET', "/v1/jobs/#{encode_id(id)}")
|
|
96
|
+
def get_transcript(id) = request('GET', "/v1/transcripts/#{encode_id(id)}")
|
|
97
|
+
def export_transcript(id, format: 'txt')
|
|
98
|
+
raise ArgumentError, 'Choose json, txt, srt or vtt.' unless %w[json txt srt vtt].include?(format)
|
|
99
|
+
request('GET', "/v1/transcripts/#{encode_id(id)}/export", query: {format: format}, text: true)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def wait_for_job(id, timeout: 120, poll_interval: 3)
|
|
103
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + positive(timeout, 'timeout')
|
|
104
|
+
positive(poll_interval, 'poll_interval')
|
|
105
|
+
last = nil
|
|
106
|
+
loop do
|
|
107
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
108
|
+
raise JobTimeoutError, last if remaining <= 0
|
|
109
|
+
last = request('GET', "/v1/jobs/#{encode_id(id)}", timeout: [remaining, @timeout].min)
|
|
110
|
+
return last if %w[succeeded failed cancelled].include?(last['status'])
|
|
111
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
112
|
+
raise JobTimeoutError, last if remaining <= 0
|
|
113
|
+
sleep [remaining, poll_interval].min
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
def positive(value, name)
|
|
119
|
+
raise ArgumentError, "#{name} must be a finite positive number." unless value.is_a?(Numeric) && value.finite? && value > 0
|
|
120
|
+
value
|
|
121
|
+
end
|
|
122
|
+
def encode_id(id)
|
|
123
|
+
raise ArgumentError, 'Provide a nonempty ID.' unless id.is_a?(String) && !id.empty? && !['.', '..'].include?(id)
|
|
124
|
+
URI.encode_www_form_component(id).gsub('+', '%20')
|
|
125
|
+
end
|
|
126
|
+
def redact(value)
|
|
127
|
+
value.to_s.gsub(@api_key, '[REDACTED]').gsub(/td_live_[A-Za-z0-9_-]+|Bearer\s+\S+/i, '[REDACTED]')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: transcriptdock
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Transcript Dock
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Standard library client for timed transcripts, jobs and the public Transcript
|
|
14
|
+
Dock API.
|
|
15
|
+
email: transcriptdock@volods.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/transcriptdock.rb
|
|
23
|
+
homepage: https://www.transcriptdock.com/docs
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata:
|
|
27
|
+
documentation_uri: https://www.transcriptdock.com/docs
|
|
28
|
+
source_code_uri: https://github.com/TranscriptDock/transcriptdock-ruby
|
|
29
|
+
bug_tracker_uri: https://github.com/TranscriptDock/transcriptdock-ruby/issues
|
|
30
|
+
rubygems_mfa_required: 'true'
|
|
31
|
+
post_install_message:
|
|
32
|
+
rdoc_options: []
|
|
33
|
+
require_paths:
|
|
34
|
+
- lib
|
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.2'
|
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
requirements: []
|
|
46
|
+
rubygems_version: 3.4.19
|
|
47
|
+
signing_key:
|
|
48
|
+
specification_version: 4
|
|
49
|
+
summary: Ruby client for the Transcript Dock transcription API
|
|
50
|
+
test_files: []
|