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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f70ea3ee7d06a49e41215d92a1b04db9173cbf357cbeccbd9bb85adc281b2e45
|
|
4
|
+
data.tar.gz: b86bcb7ff7b2f9eab771a2e3419b1ab0cf42a08bb4ae6d4bd906d2cee0ea0c5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d2687f91dbe8ca128de93e975de5e0a25359e8e8f1d7399b6092721b553ac26985eeff2d2963a7112f70bf11f98fdbcfff8f30e4462cb9fc9b3375349e6cf37
|
|
7
|
+
data.tar.gz: e303be1550bd6d5f98635689316e9be8e17d88657e196d614133f84aad0674745db45ffb0f1702dfc22ac3f5798e923113f612151fe98a71bfbce8ba602b0542
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
class Classify < Command
|
|
7
|
+
class UnsupportedSource < StandardError; end
|
|
8
|
+
class ResolutionError < StandardError; end
|
|
9
|
+
|
|
10
|
+
EXTENSION_MAP = {
|
|
11
|
+
".txt" => :text_file,
|
|
12
|
+
".md" => :text_file,
|
|
13
|
+
".rst" => :text_file,
|
|
14
|
+
".pdf" => :pdf_file,
|
|
15
|
+
".mp3" => :audio_file
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(input:)
|
|
19
|
+
@input = input
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
success(payload: classified)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :input
|
|
29
|
+
|
|
30
|
+
def classified
|
|
31
|
+
return RawText.new(text: input) unless resolved_path || url?
|
|
32
|
+
|
|
33
|
+
return Sources::Url.new(input) if url?
|
|
34
|
+
|
|
35
|
+
case file_kind
|
|
36
|
+
when :pdf_file then Sources::Pdf.new(resolved_path)
|
|
37
|
+
when :text_file then Sources::Text.new(resolved_path)
|
|
38
|
+
when :audio_file then Sources::Audio.new(resolved_path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def url?
|
|
43
|
+
uri = URI.parse(input)
|
|
44
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
45
|
+
rescue URI::InvalidURIError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def file_kind
|
|
50
|
+
ext = File.extname(resolved_path).downcase
|
|
51
|
+
EXTENSION_MAP.fetch(ext) do
|
|
52
|
+
raise UnsupportedSource, "Unsupported file type: #{ext}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def resolved_path
|
|
57
|
+
@resolved_path ||= begin
|
|
58
|
+
result = Detection::PathResolver.call(raw: input)
|
|
59
|
+
raise ResolutionError, result.error.message unless result.success?
|
|
60
|
+
|
|
61
|
+
result.payload
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Cli
|
|
7
|
+
class Runner < Command
|
|
8
|
+
ExecutionError = Class.new(Error)
|
|
9
|
+
|
|
10
|
+
def initialize(argv:)
|
|
11
|
+
@argv = argv
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
stdout, stderr, status = Open3.capture3(*argv)
|
|
16
|
+
|
|
17
|
+
raise ExecutionError, stderr unless status.success?
|
|
18
|
+
|
|
19
|
+
success(payload: stdout)
|
|
20
|
+
rescue ExecutionError
|
|
21
|
+
raise
|
|
22
|
+
rescue => e
|
|
23
|
+
raise ExecutionError, e.message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :argv
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
Result = Data.define(:payload, :error) do
|
|
5
|
+
def success? = error.nil?
|
|
6
|
+
def failure? = !error.nil?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Command
|
|
10
|
+
def self.call(...)
|
|
11
|
+
new(...).call
|
|
12
|
+
rescue => e
|
|
13
|
+
Result.new(payload: nil, error: e)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success(payload:)
|
|
17
|
+
Result.new(payload: payload, error: nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def failure(error)
|
|
21
|
+
Result.new(payload: nil, error: error)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Configuration
|
|
5
|
+
class << self
|
|
6
|
+
def deepgram_api_key
|
|
7
|
+
@deepgram_api_key ||= ENV.fetch("DEEPGRAM_API_KEY", nil)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_writer :deepgram_api_key
|
|
11
|
+
|
|
12
|
+
def configure
|
|
13
|
+
yield self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset!
|
|
17
|
+
@deepgram_api_key = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Detection
|
|
7
|
+
class PathResolver < Command
|
|
8
|
+
def initialize(raw:)
|
|
9
|
+
@raw = raw
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
path = candidates.find { |p| p.file? }
|
|
14
|
+
success(payload: path&.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :raw
|
|
20
|
+
|
|
21
|
+
def candidates
|
|
22
|
+
[raw, shell_unescape].compact.uniq.map { |s| Pathname.new(s).expand_path }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def shell_unescape
|
|
26
|
+
parts = Shellwords.shellsplit(raw)
|
|
27
|
+
parts.first if parts.length == 1
|
|
28
|
+
rescue ArgumentError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
class Distill < Command
|
|
5
|
+
def initialize(content:)
|
|
6
|
+
@content = content
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
case content
|
|
11
|
+
in RawText(text:)
|
|
12
|
+
Handlers::RawText.call(text: text)
|
|
13
|
+
in TextContent(source_ref:, text:)
|
|
14
|
+
Handlers::TextFile.call(text: text, source_ref: source_ref)
|
|
15
|
+
in ArticleContent(source_ref:, text:, title:, metadata:)
|
|
16
|
+
Handlers::Article.call(content: content)
|
|
17
|
+
in PageCollection(source_ref:, pages:)
|
|
18
|
+
document = Mediums::Pdf::Document.new(source_ref.locator, source_ref: source_ref)
|
|
19
|
+
Handlers::Pdf.call(document: document, pages: pages)
|
|
20
|
+
in Transcripts::Content(source_ref:, segments:, metadata:)
|
|
21
|
+
Handlers::Transcript.call(content: content)
|
|
22
|
+
else
|
|
23
|
+
failure(ArgumentError.new("Unsupported content type: #{content.class}"))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :content
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hexapdf"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Substrate
|
|
7
|
+
class ExtractPdf < Command
|
|
8
|
+
def initialize(bytes:, source_ref:, limits: Mediums::Pdf::Limits.new, page_separator: "\n\n")
|
|
9
|
+
@bytes = bytes
|
|
10
|
+
@source_ref = source_ref
|
|
11
|
+
@limits = limits
|
|
12
|
+
@page_separator = page_separator
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
validate_input
|
|
17
|
+
validate_page_separator
|
|
18
|
+
extract_document
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate_input
|
|
24
|
+
raise ArgumentError, "bytes must be a String" unless @bytes.is_a?(String)
|
|
25
|
+
@limits.check!(:input_bytes, @bytes.bytesize)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate_page_separator
|
|
29
|
+
raise ArgumentError, "page_separator must be a String" unless @page_separator.is_a?(String)
|
|
30
|
+
@page_separator = @page_separator.encode(Encoding::UTF_8).dup.freeze
|
|
31
|
+
raise ArgumentError, "page_separator must contain valid UTF-8" unless @page_separator.valid_encoding?
|
|
32
|
+
rescue EncodingError => error
|
|
33
|
+
raise ArgumentError, "page_separator must contain valid UTF-8", cause: error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_document
|
|
37
|
+
StringIO.open(@bytes) do |io|
|
|
38
|
+
document = HexaPDF::Document.new(io: io, config: {"document.auto_decrypt" => false, "filter.map" => {BrotliDecode: nil}})
|
|
39
|
+
raise Mediums::Pdf::EncryptedPdf, "Encrypted PDFs are not supported" if document.encrypted?
|
|
40
|
+
objects = page_objects(document)
|
|
41
|
+
pages = extract_pages(objects)
|
|
42
|
+
outline = Mediums::Pdf::OutlineFacts.new(document, objects, @limits).call
|
|
43
|
+
publish_content(pages, outline)
|
|
44
|
+
end
|
|
45
|
+
rescue HexaPDF::Error => error
|
|
46
|
+
raise Mediums::Pdf::InvalidText, "PDF Unicode mapping is invalid", cause: error if error.cause.is_a?(EncodingError)
|
|
47
|
+
raise Mediums::Pdf::InvalidPdf, "PDF parsing or page extraction failed", cause: error
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def page_objects(document)
|
|
51
|
+
root = document.catalog[:Pages]
|
|
52
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page tree is missing" unless root.is_a?(HexaPDF::Dictionary)
|
|
53
|
+
count = root[:Count]
|
|
54
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page count is invalid" unless count.is_a?(Integer) && count >= 0
|
|
55
|
+
@limits.check!(:pages, count)
|
|
56
|
+
pending = [[root, nil]]
|
|
57
|
+
seen = {}
|
|
58
|
+
[].tap do |pages|
|
|
59
|
+
until pending.empty?
|
|
60
|
+
candidate, parent = pending.pop
|
|
61
|
+
node = document.deref(candidate)
|
|
62
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page tree node is invalid" unless node.is_a?(HexaPDF::Dictionary)
|
|
63
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page ancestry is inconsistent" unless node[:Parent] == parent
|
|
64
|
+
identity = node.indirect? ? [node.oid, node.gen] : node.data.object_id
|
|
65
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page tree contains a cycle or repeated node" if seen[identity]
|
|
66
|
+
seen[identity] = true
|
|
67
|
+
if node[:Type] == :Page
|
|
68
|
+
@limits.check!(:pages, pages.size + 1)
|
|
69
|
+
pages << document.wrap(node, type: :Page)
|
|
70
|
+
elsif node[:Type] == :Pages
|
|
71
|
+
kids = node[:Kids]
|
|
72
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page tree children are invalid" unless kids.is_a?(HexaPDF::PDFArray)
|
|
73
|
+
pending.concat(kids.to_a.reverse.map { |child| [child, node] })
|
|
74
|
+
else
|
|
75
|
+
raise Mediums::Pdf::InvalidPdf, "PDF page tree type is invalid"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_pages(objects)
|
|
82
|
+
@text = +""
|
|
83
|
+
objects.map.with_index do |page, index|
|
|
84
|
+
@limits.check!(:pages, index + 1)
|
|
85
|
+
text = page.extract_text.encode(Encoding::UTF_8)
|
|
86
|
+
raise Mediums::Pdf::InvalidText, "PDF text contains invalid UTF-8 or NUL" unless text.valid_encoding? && !text.include?("\0")
|
|
87
|
+
append_page_text(text, index)
|
|
88
|
+
Mediums::Pdf::Page.new(number: index + 1, text: text.freeze, crop_box: crop_box(page))
|
|
89
|
+
end.freeze.tap do |pages|
|
|
90
|
+
raise Mediums::Pdf::NoSelectableText, "PDF contains no selectable text" if pages.all? { |page| page.text.match?(/\A[[:space:]]*\z/) }
|
|
91
|
+
end
|
|
92
|
+
rescue EncodingError => error
|
|
93
|
+
raise Mediums::Pdf::InvalidText, "PDF text contains invalid UTF-8", cause: error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def append_page_text(text, index)
|
|
97
|
+
separator = index.zero? ? "" : @page_separator
|
|
98
|
+
@limits.check!(:text_bytes, @text.bytesize + separator.bytesize + text.bytesize)
|
|
99
|
+
@text << separator << text
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def crop_box(page)
|
|
103
|
+
media, crop = page_rectangles(page)
|
|
104
|
+
return unless usable_box?(media) && usable_box?(crop)
|
|
105
|
+
box = Mediums::Pdf::Box.new(left: [media.left, crop.left].max,
|
|
106
|
+
bottom: [media.bottom, crop.bottom].max, right: [media.right, crop.right].min,
|
|
107
|
+
top: [media.top, crop.top].min)
|
|
108
|
+
box if box.right > box.left && box.top > box.bottom
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def page_rectangles(page)
|
|
112
|
+
media = page[:MediaBox]
|
|
113
|
+
crop = page[:CropBox]
|
|
114
|
+
[media, crop.nil? ? media : crop]
|
|
115
|
+
rescue HexaPDF::Error
|
|
116
|
+
[nil, nil]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def usable_box?(rectangle)
|
|
120
|
+
rectangle.is_a?(HexaPDF::Rectangle) &&
|
|
121
|
+
rectangle.to_a.all? { |value| value.is_a?(Numeric) && value.finite? } &&
|
|
122
|
+
rectangle.right > rectangle.left && rectangle.top > rectangle.bottom
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def publish_content(pages, outline)
|
|
126
|
+
metadata = {extractor: "HexaPDF", extractor_version: HexaPDF::VERSION.dup.freeze,
|
|
127
|
+
input_bytes: @bytes.bytesize, page_count: pages.size,
|
|
128
|
+
text_bytes: @text.bytesize}.freeze
|
|
129
|
+
success(payload: Mediums::Pdf::Content.new(pages: pages, source_ref: @source_ref,
|
|
130
|
+
metadata: metadata, outline: outline, text: @text.freeze))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Handlers
|
|
5
|
+
class Article < Command
|
|
6
|
+
def initialize(content:)
|
|
7
|
+
@content = content
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
success(payload: extraction)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :content
|
|
17
|
+
|
|
18
|
+
def extraction
|
|
19
|
+
Extraction.new(
|
|
20
|
+
text: content.text,
|
|
21
|
+
source: source,
|
|
22
|
+
metadata: extraction_metadata
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def source
|
|
27
|
+
Source.new(
|
|
28
|
+
kind: :article,
|
|
29
|
+
locator: content.source_ref.locator,
|
|
30
|
+
metadata: content.metadata
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def extraction_metadata
|
|
35
|
+
{title: content.title}.compact
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Handlers
|
|
5
|
+
class Pdf < Command
|
|
6
|
+
def initialize(document:, pages: nil)
|
|
7
|
+
@document = document
|
|
8
|
+
@pages = pages
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
range = pages || (1..document.page_count)
|
|
13
|
+
text = document.pages_text(range)
|
|
14
|
+
source = Source.new(kind: :pdf_file, locator: nil, metadata: {
|
|
15
|
+
page_count: document.page_count
|
|
16
|
+
})
|
|
17
|
+
extraction = Extraction.new(text: text, source: source, metadata: {
|
|
18
|
+
pages_extracted: range
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
success(payload: extraction)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :document, :pages
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Handlers
|
|
5
|
+
class RawText < Command
|
|
6
|
+
def initialize(text:)
|
|
7
|
+
@text = text
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
source = Source.new(kind: :raw_text, locator: nil, metadata: {})
|
|
12
|
+
extraction = Extraction.new(text: text, source: source, metadata: {})
|
|
13
|
+
|
|
14
|
+
success(payload: extraction)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :text
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Handlers
|
|
5
|
+
class TextFile < Command
|
|
6
|
+
def initialize(text:, source_ref:)
|
|
7
|
+
@text = text
|
|
8
|
+
@source_ref = source_ref
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
source = Source.new(kind: :text_file, locator: source_ref.locator, metadata: {
|
|
13
|
+
line_count: text.count("\n"),
|
|
14
|
+
file_size: File.size(source_ref.locator)
|
|
15
|
+
})
|
|
16
|
+
extraction = Extraction.new(text: text, source: source, metadata: {})
|
|
17
|
+
|
|
18
|
+
success(payload: extraction)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :text, :source_ref
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Handlers
|
|
5
|
+
class Transcript < Command
|
|
6
|
+
def initialize(content:)
|
|
7
|
+
@content = content
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
success(payload: extraction)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :content
|
|
17
|
+
|
|
18
|
+
def extraction
|
|
19
|
+
Extraction.new(
|
|
20
|
+
text: full_text,
|
|
21
|
+
source: source,
|
|
22
|
+
metadata: extraction_metadata
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def full_text
|
|
27
|
+
content.segments.map(&:text).join("\n\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def source
|
|
31
|
+
Source.new(
|
|
32
|
+
kind: :audio,
|
|
33
|
+
locator: content.source_ref.locator,
|
|
34
|
+
metadata: content.metadata
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extraction_metadata
|
|
39
|
+
{
|
|
40
|
+
segment_count: content.segments.count,
|
|
41
|
+
duration_seconds: content.metadata.duration_seconds,
|
|
42
|
+
language: content.metadata.language,
|
|
43
|
+
diarized: content.metadata.diarized
|
|
44
|
+
}.compact
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|