substrate 0.1.0 → 0.3.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 +4 -4
- data/lib/substrate/classify.rb +65 -0
- data/lib/substrate/cli/availability.rb +11 -0
- data/lib/substrate/cli/error.rb +7 -0
- data/lib/substrate/cli/runner.rb +31 -0
- data/lib/substrate/command.rb +24 -0
- data/lib/substrate/configuration.rb +21 -0
- data/lib/substrate/detection/path_resolver.rb +33 -0
- data/lib/substrate/distill.rb +31 -0
- data/lib/substrate/extract_pdf.rb +133 -0
- data/lib/substrate/handlers/article.rb +39 -0
- data/lib/substrate/handlers/pdf.rb +29 -0
- data/lib/substrate/handlers/raw_text.rb +22 -0
- data/lib/substrate/handlers/text_file.rb +26 -0
- data/lib/substrate/handlers/transcript.rb +48 -0
- data/lib/substrate/mediums/pdf/box.rb +9 -0
- data/lib/substrate/mediums/pdf/chapter.rb +9 -0
- data/lib/substrate/mediums/pdf/content.rb +9 -0
- data/lib/substrate/mediums/pdf/destination.rb +9 -0
- data/lib/substrate/mediums/pdf/destination_resolver.rb +131 -0
- data/lib/substrate/mediums/pdf/document.rb +46 -0
- data/lib/substrate/mediums/pdf/encrypted_pdf.rb +9 -0
- data/lib/substrate/mediums/pdf/entry.rb +9 -0
- data/lib/substrate/mediums/pdf/invalid_pdf.rb +9 -0
- data/lib/substrate/mediums/pdf/invalid_text.rb +9 -0
- data/lib/substrate/mediums/pdf/limit_exceeded.rb +17 -0
- data/lib/substrate/mediums/pdf/limits.rb +21 -0
- data/lib/substrate/mediums/pdf/named_destinations.rb +94 -0
- data/lib/substrate/mediums/pdf/no_selectable_text.rb +9 -0
- data/lib/substrate/mediums/pdf/outline.rb +9 -0
- data/lib/substrate/mediums/pdf/outline_facts.rb +115 -0
- data/lib/substrate/mediums/pdf/outline_parser.rb +67 -0
- data/lib/substrate/mediums/pdf/page.rb +9 -0
- data/lib/substrate/mediums/pdf/page_extractor.rb +28 -0
- data/lib/substrate/mediums/pdf/reader.rb +40 -0
- data/lib/substrate/mediums/pdf/text_extractor.rb +36 -0
- data/lib/substrate/primitives/article_content.rb +10 -0
- data/lib/substrate/primitives/extraction.rb +5 -0
- data/lib/substrate/primitives/metadata.rb +21 -0
- data/lib/substrate/primitives/page_collection.rb +5 -0
- data/lib/substrate/primitives/pdf_file.rb +5 -0
- data/lib/substrate/primitives/raw_text.rb +5 -0
- data/lib/substrate/primitives/source.rb +5 -0
- data/lib/substrate/primitives/text_content.rb +5 -0
- data/lib/substrate/primitives/text_file.rb +5 -0
- data/lib/substrate/primitives/transcripts/content.rb +11 -0
- data/lib/substrate/primitives/transcripts/metadata.rb +25 -0
- data/lib/substrate/primitives/transcripts/segment.rb +25 -0
- data/lib/substrate/primitives/transcripts/word.rb +23 -0
- data/lib/substrate/primitives/url_candidate.rb +13 -0
- data/lib/substrate/source_ref.rb +5 -0
- data/lib/substrate/sources/article.rb +34 -0
- data/lib/substrate/sources/audio.rb +57 -0
- data/lib/substrate/sources/pdf.rb +35 -0
- data/lib/substrate/sources/text.rb +23 -0
- data/lib/substrate/sources/url.rb +36 -0
- data/lib/substrate/transcribers/deepgram/client.rb +70 -0
- data/lib/substrate/transcribers/deepgram/configuration.rb +41 -0
- data/lib/substrate/transcribers/deepgram/response_parser.rb +102 -0
- data/lib/substrate/transcribers/deepgram.rb +45 -0
- data/lib/substrate/transcribers/whisper_cpp/response_parser.rb +66 -0
- data/lib/substrate/transcribers/whisper_cpp.rb +87 -0
- data/lib/substrate/transcripts/select_time_range.rb +59 -0
- data/lib/substrate/url_resolvers/generic.rb +71 -0
- data/lib/substrate/url_resolvers/resolver_for.rb +35 -0
- data/lib/substrate/version.rb +1 -1
- data/lib/substrate.rb +16 -3
- metadata +107 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
Metadata = Data.define(
|
|
5
|
+
:requested_url,
|
|
6
|
+
:final_url,
|
|
7
|
+
:redirected,
|
|
8
|
+
:content_type,
|
|
9
|
+
:extractor
|
|
10
|
+
) do
|
|
11
|
+
def initialize(
|
|
12
|
+
requested_url:,
|
|
13
|
+
final_url: nil,
|
|
14
|
+
redirected: false,
|
|
15
|
+
content_type: nil,
|
|
16
|
+
extractor: nil
|
|
17
|
+
)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcripts
|
|
5
|
+
Metadata = Data.define(
|
|
6
|
+
:provider,
|
|
7
|
+
:model,
|
|
8
|
+
:language,
|
|
9
|
+
:duration_seconds,
|
|
10
|
+
:diarized,
|
|
11
|
+
:word_timestamps
|
|
12
|
+
) do
|
|
13
|
+
def initialize(
|
|
14
|
+
provider:,
|
|
15
|
+
model:,
|
|
16
|
+
language: nil,
|
|
17
|
+
duration_seconds: nil,
|
|
18
|
+
diarized: false,
|
|
19
|
+
word_timestamps: false
|
|
20
|
+
)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcripts
|
|
5
|
+
Segment = Data.define(
|
|
6
|
+
:text,
|
|
7
|
+
:start,
|
|
8
|
+
:end,
|
|
9
|
+
:confidence,
|
|
10
|
+
:speaker,
|
|
11
|
+
:words
|
|
12
|
+
) do
|
|
13
|
+
def initialize(
|
|
14
|
+
text:,
|
|
15
|
+
start:,
|
|
16
|
+
end:,
|
|
17
|
+
confidence: nil,
|
|
18
|
+
speaker: nil,
|
|
19
|
+
words: []
|
|
20
|
+
)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcripts
|
|
5
|
+
Word = Data.define(
|
|
6
|
+
:text,
|
|
7
|
+
:start,
|
|
8
|
+
:end,
|
|
9
|
+
:confidence,
|
|
10
|
+
:speaker
|
|
11
|
+
) do
|
|
12
|
+
def initialize(
|
|
13
|
+
text:,
|
|
14
|
+
start:,
|
|
15
|
+
end:,
|
|
16
|
+
confidence: nil,
|
|
17
|
+
speaker: nil
|
|
18
|
+
)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Sources
|
|
5
|
+
class Article
|
|
6
|
+
def initialize(url:, text:, title:, metadata:)
|
|
7
|
+
@url = url
|
|
8
|
+
@text = text
|
|
9
|
+
@title = title
|
|
10
|
+
@metadata = metadata
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def source_ref
|
|
14
|
+
@source_ref ||= SourceRef.new(
|
|
15
|
+
kind: :article,
|
|
16
|
+
locator: metadata.final_url || url
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def body
|
|
21
|
+
ArticleContent.new(
|
|
22
|
+
source_ref: source_ref,
|
|
23
|
+
text: text,
|
|
24
|
+
title: title,
|
|
25
|
+
metadata: metadata
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :url, :text, :title, :metadata
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Sources
|
|
5
|
+
class Audio
|
|
6
|
+
class TranscriptionError < StandardError; end
|
|
7
|
+
|
|
8
|
+
DEFAULT_PROVIDER = :deepgram
|
|
9
|
+
|
|
10
|
+
def initialize(path, provider: DEFAULT_PROVIDER, **options)
|
|
11
|
+
@path = path
|
|
12
|
+
@provider = provider
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def source_ref
|
|
17
|
+
@source_ref ||= SourceRef.new(kind: :audio, locator: path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def body
|
|
21
|
+
@body ||= begin
|
|
22
|
+
transcribe
|
|
23
|
+
rescue TranscriptionError => e
|
|
24
|
+
raise TranscriptionError, e.message
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def select_time_range(range)
|
|
29
|
+
result = Transcripts::SelectTimeRange.call(content: body, range: range)
|
|
30
|
+
raise result.error if result.failure?
|
|
31
|
+
|
|
32
|
+
result.payload
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :path, :provider, :options
|
|
38
|
+
|
|
39
|
+
def transcribe
|
|
40
|
+
provider_class = resolve_provider(provider)
|
|
41
|
+
result = provider_class.call(path: path, **options)
|
|
42
|
+
raise TranscriptionError, result.error.message unless result.success?
|
|
43
|
+
|
|
44
|
+
result.payload
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_provider(name)
|
|
48
|
+
case name
|
|
49
|
+
when :deepgram then Transcribers::Deepgram
|
|
50
|
+
when :whisper_cpp then Transcribers::WhisperCpp
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError, "Unknown transcription provider: #{name}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Sources
|
|
5
|
+
class Pdf
|
|
6
|
+
def initialize(path)
|
|
7
|
+
@path = path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def source_ref
|
|
11
|
+
@source_ref ||= SourceRef.new(kind: :pdf, locator: path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def chapters
|
|
15
|
+
document.chapters
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def page_count
|
|
19
|
+
document.page_count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def select_pages(range)
|
|
23
|
+
PageCollection.new(source_ref: source_ref, pages: range)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :path
|
|
29
|
+
|
|
30
|
+
def document
|
|
31
|
+
@document ||= Mediums::Pdf::Document.new(path, source_ref: source_ref)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Sources
|
|
5
|
+
class Text
|
|
6
|
+
def initialize(path)
|
|
7
|
+
@path = path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def source_ref
|
|
11
|
+
@source_ref ||= SourceRef.new(kind: :text, locator: path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
TextContent.new(source_ref: source_ref, text: File.read(path))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Sources
|
|
7
|
+
class Url
|
|
8
|
+
class ResolutionError < StandardError; end
|
|
9
|
+
|
|
10
|
+
def initialize(url)
|
|
11
|
+
@url = url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def source_ref
|
|
15
|
+
@source_ref ||= SourceRef.new(kind: :url, locator: url)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def candidates
|
|
19
|
+
@candidates ||= begin
|
|
20
|
+
result = UrlResolvers::ResolverFor.call(url: url)
|
|
21
|
+
raise ResolutionError, result.error.message unless result.success?
|
|
22
|
+
|
|
23
|
+
result.payload
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def primary_candidate
|
|
28
|
+
candidates.min_by(&:rank)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :url
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
|
|
6
|
+
module Substrate
|
|
7
|
+
module Transcribers
|
|
8
|
+
class Deepgram
|
|
9
|
+
class Client
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
BASE_URL = "https://api.deepgram.com"
|
|
13
|
+
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
14
|
+
MAX_RETRIES = 3
|
|
15
|
+
DEFAULT_TIMEOUT = 120
|
|
16
|
+
|
|
17
|
+
def initialize(api_key:, timeout: DEFAULT_TIMEOUT, retry_interval: 1)
|
|
18
|
+
@api_key = api_key
|
|
19
|
+
@connection = build_connection(timeout: timeout, retry_interval: retry_interval)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def transcribe(path:, params:)
|
|
23
|
+
response = connection.post("/v1/listen", File.binread(path)) do |req|
|
|
24
|
+
req.headers["Content-Type"] = content_type(path)
|
|
25
|
+
req.params = params
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
raise Error, "API error: #{response.status} #{response.body}" unless response.success?
|
|
29
|
+
|
|
30
|
+
response.body
|
|
31
|
+
rescue Faraday::Error => e
|
|
32
|
+
raise Error, "Request failed: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :connection, :api_key
|
|
38
|
+
|
|
39
|
+
def build_connection(timeout:, retry_interval:)
|
|
40
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
41
|
+
f.response :raise_error
|
|
42
|
+
f.response :json, content_type: /\bjson$/
|
|
43
|
+
|
|
44
|
+
f.request :retry,
|
|
45
|
+
max: MAX_RETRIES,
|
|
46
|
+
interval: retry_interval,
|
|
47
|
+
backoff_factor: 2,
|
|
48
|
+
retry_statuses: RETRYABLE_STATUSES
|
|
49
|
+
|
|
50
|
+
f.headers["Authorization"] = "Token #{api_key}"
|
|
51
|
+
|
|
52
|
+
f.options.timeout = timeout
|
|
53
|
+
f.options.open_timeout = timeout / 2
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def content_type(path)
|
|
58
|
+
case File.extname(path).downcase
|
|
59
|
+
when ".mp3" then "audio/mp3"
|
|
60
|
+
when ".wav" then "audio/wav"
|
|
61
|
+
when ".m4a" then "audio/m4a"
|
|
62
|
+
when ".ogg" then "audio/ogg"
|
|
63
|
+
when ".flac" then "audio/flac"
|
|
64
|
+
else "audio/mp3"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcribers
|
|
5
|
+
class Deepgram
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_reader :api_key, :language, :diarize, :punctuate, :model
|
|
8
|
+
|
|
9
|
+
DEFAULT_MODEL = "nova-2"
|
|
10
|
+
|
|
11
|
+
def initialize(
|
|
12
|
+
language: nil,
|
|
13
|
+
diarize: false,
|
|
14
|
+
punctuate: true,
|
|
15
|
+
model: DEFAULT_MODEL
|
|
16
|
+
)
|
|
17
|
+
@api_key = Substrate::Configuration.deepgram_api_key
|
|
18
|
+
@language = language
|
|
19
|
+
@diarize = !!diarize
|
|
20
|
+
@punctuate = !!punctuate
|
|
21
|
+
@model = model
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
!api_key.to_s.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_params
|
|
29
|
+
{
|
|
30
|
+
model: model,
|
|
31
|
+
smart_format: true,
|
|
32
|
+
punctuate: punctuate,
|
|
33
|
+
utterances: true,
|
|
34
|
+
language: language,
|
|
35
|
+
diarize: diarize || nil
|
|
36
|
+
}.compact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcribers
|
|
5
|
+
class Deepgram
|
|
6
|
+
class ResponseParser
|
|
7
|
+
class MalformedResponseError < StandardError; end
|
|
8
|
+
|
|
9
|
+
def initialize(response, path:, diarized: false)
|
|
10
|
+
@response = response
|
|
11
|
+
@path = path
|
|
12
|
+
@diarized = diarized
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse
|
|
16
|
+
Transcripts::Content.new(
|
|
17
|
+
source_ref: source_ref,
|
|
18
|
+
segments: segments,
|
|
19
|
+
metadata: metadata
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :response, :path, :diarized
|
|
26
|
+
|
|
27
|
+
def alternatives
|
|
28
|
+
response.dig("results", "channels", 0, "alternatives", 0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def metadata
|
|
32
|
+
Transcripts::Metadata.new(
|
|
33
|
+
provider: :deepgram,
|
|
34
|
+
model: model_name,
|
|
35
|
+
language: response.dig("metadata", "language"),
|
|
36
|
+
duration_seconds: response.dig("metadata", "duration"),
|
|
37
|
+
diarized: diarized,
|
|
38
|
+
word_timestamps: words.any?
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def model_name
|
|
43
|
+
model_info.values.first&.fetch("arch", nil) || Configuration::DEFAULT_MODEL
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def segments
|
|
47
|
+
return utterances.map { |utterance| utterance_to_segment(utterance) } if utterances.any?
|
|
48
|
+
|
|
49
|
+
[words_to_segment(alternatives["transcript"], words)]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def words
|
|
53
|
+
alternatives["words"] || []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def utterance_to_segment(utterance)
|
|
57
|
+
Transcripts::Segment.new(
|
|
58
|
+
text: utterance["transcript"],
|
|
59
|
+
start: utterance["start"],
|
|
60
|
+
end: utterance["end"],
|
|
61
|
+
confidence: utterance["confidence"],
|
|
62
|
+
speaker: utterance["speaker"],
|
|
63
|
+
words: utterance.fetch("words", []).map { |w| word_data_to_word(w) }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def words_to_segment(transcript, word_list)
|
|
68
|
+
Transcripts::Segment.new(
|
|
69
|
+
text: transcript,
|
|
70
|
+
start: word_list.first["start"],
|
|
71
|
+
end: word_list.last["end"],
|
|
72
|
+
confidence: nil,
|
|
73
|
+
speaker: nil,
|
|
74
|
+
words: word_list.map { |w| word_data_to_word(w) }
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def word_data_to_word(word_data)
|
|
79
|
+
Transcripts::Word.new(
|
|
80
|
+
text: word_data["punctuated_word"] || word_data["word"],
|
|
81
|
+
start: word_data["start"],
|
|
82
|
+
end: word_data["end"],
|
|
83
|
+
confidence: word_data["confidence"],
|
|
84
|
+
speaker: word_data["speaker"]
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def utterances
|
|
89
|
+
response.dig("results", "utterances") || []
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def model_info
|
|
93
|
+
response.dig("metadata", "model_info") || {}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def source_ref
|
|
97
|
+
@source_ref ||= SourceRef.new(kind: :audio, locator: path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcribers
|
|
5
|
+
class Deepgram < Command
|
|
6
|
+
class TranscriptionError < StandardError; end
|
|
7
|
+
|
|
8
|
+
def initialize(path:, **options)
|
|
9
|
+
@path = path
|
|
10
|
+
@config = Configuration.new(**options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
return failure(missing_key_error) unless config.valid?
|
|
15
|
+
|
|
16
|
+
response = client.transcribe(path: path, params: config.to_params)
|
|
17
|
+
content = parser(response).parse
|
|
18
|
+
|
|
19
|
+
success(payload: content)
|
|
20
|
+
rescue Client::Error => e
|
|
21
|
+
failure(transcription_error(e.message))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :path, :config
|
|
27
|
+
|
|
28
|
+
def client
|
|
29
|
+
@client ||= Client.new(api_key: config.api_key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parser(response)
|
|
33
|
+
ResponseParser.new(response, path: path, diarized: config.diarize)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def missing_key_error
|
|
37
|
+
ArgumentError.new("Deepgram API key required. Configure with Substrate::Configuration.deepgram_api_key = 'your_key'")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def transcription_error(message)
|
|
41
|
+
TranscriptionError.new(message)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Transcribers
|
|
5
|
+
class WhisperCpp
|
|
6
|
+
class ResponseParser < Command
|
|
7
|
+
def initialize(output:, original_path:)
|
|
8
|
+
@output = output
|
|
9
|
+
@original_path = original_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
success(payload: parse)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :output, :original_path
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
Transcripts::Content.new(
|
|
22
|
+
source_ref: source_ref,
|
|
23
|
+
segments: segments,
|
|
24
|
+
metadata: metadata
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def segments
|
|
29
|
+
transcription.map do |segment|
|
|
30
|
+
Transcripts::Segment.new(
|
|
31
|
+
text: segment.fetch("text"),
|
|
32
|
+
start: segment["offsets"]["from"] / 1000.0,
|
|
33
|
+
end: segment["offsets"]["to"] / 1000.0,
|
|
34
|
+
confidence: nil,
|
|
35
|
+
speaker: nil,
|
|
36
|
+
words: []
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def metadata
|
|
42
|
+
Transcripts::Metadata.new(
|
|
43
|
+
provider: :whisper_cpp,
|
|
44
|
+
model: params.fetch("model", "unknown"),
|
|
45
|
+
language: output.fetch("result", {})["language"] || params["language"],
|
|
46
|
+
duration_seconds: nil,
|
|
47
|
+
diarized: false,
|
|
48
|
+
word_timestamps: false
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def transcription
|
|
53
|
+
output.fetch("transcription")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def params
|
|
57
|
+
output.fetch("params", {})
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def source_ref
|
|
61
|
+
SourceRef.new(kind: :audio, locator: original_path)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|