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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +65 -0
  4. data/lib/transcriptdock.rb +130 -0
  5. 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: []