substrate 0.1.0 → 0.2.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 +95 -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/chapter.rb +9 -0
- data/lib/substrate/mediums/pdf/content.rb +13 -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 +106 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
class Document
|
|
7
|
+
def initialize(path, source_ref:)
|
|
8
|
+
@path = path
|
|
9
|
+
@source_ref = source_ref
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def chapters
|
|
13
|
+
outline_parser.chapters
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def page_count
|
|
17
|
+
reader.page_count
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def page_text(number)
|
|
21
|
+
page_extractor.text_for_page(number)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pages_text(range)
|
|
25
|
+
page_extractor.text_for_pages(range)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :path, :source_ref
|
|
31
|
+
|
|
32
|
+
def reader
|
|
33
|
+
@reader ||= Reader.new(path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def outline_parser
|
|
37
|
+
@outline_parser ||= OutlineParser.new(reader, source_ref)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def page_extractor
|
|
41
|
+
@page_extractor ||= PageExtractor.new(reader)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
class LimitExceeded < StandardError
|
|
7
|
+
attr_reader :dimension, :limit
|
|
8
|
+
|
|
9
|
+
def initialize(dimension, limit)
|
|
10
|
+
@dimension = dimension
|
|
11
|
+
@limit = limit
|
|
12
|
+
super("PDF exceeds #{dimension} limit of #{limit}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
Limits = Data.define(:input_bytes, :pages, :text_bytes, :outline_entries) do
|
|
7
|
+
def initialize(input_bytes: 25 * 1024 * 1024, pages: 2000,
|
|
8
|
+
text_bytes: 32 * 1024 * 1024, outline_entries: 2000)
|
|
9
|
+
values = {input_bytes: input_bytes, pages: pages, text_bytes: text_bytes, outline_entries: outline_entries}
|
|
10
|
+
raise ArgumentError, "PDF limits must be positive integers" unless values.values.all? { |value| value.is_a?(Integer) && value.positive? }
|
|
11
|
+
super(**values)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check!(dimension, actual)
|
|
15
|
+
maximum = public_send(dimension)
|
|
16
|
+
raise LimitExceeded.new(dimension, maximum) if actual > maximum
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
class NamedDestinations
|
|
7
|
+
Lookup = Data.define(:value, :reason)
|
|
8
|
+
|
|
9
|
+
def initialize(document, limits)
|
|
10
|
+
@document = document
|
|
11
|
+
@limit = limits.outline_entries
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def lookup(name)
|
|
15
|
+
Lookup.new(value: named_destination(name), reason: nil)
|
|
16
|
+
rescue LookupFailure => error
|
|
17
|
+
Lookup.new(value: nil, reason: error.reason)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def name_matches?(key, name)
|
|
23
|
+
return false unless key.is_a?(String) || key.is_a?(Symbol)
|
|
24
|
+
text = key.to_s.encode(Encoding::UTF_8)
|
|
25
|
+
text.valid_encoding? && !text.include?("\0") && text == name
|
|
26
|
+
rescue EncodingError
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class LookupFailure < StandardError
|
|
31
|
+
attr_reader :reason
|
|
32
|
+
|
|
33
|
+
def initialize(reason)
|
|
34
|
+
@reason = reason
|
|
35
|
+
super(reason.to_s)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def named_destination(name)
|
|
40
|
+
if name.is_a?(Symbol)
|
|
41
|
+
old = @document.wrap(@document.catalog[:Dests])
|
|
42
|
+
old[name] if old.value.is_a?(Hash)
|
|
43
|
+
else
|
|
44
|
+
names = @document.wrap(@document.catalog[:Names])
|
|
45
|
+
find_name(names[:Dests], name) if names.value.is_a?(Hash) && names[:Dests]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_name(root, name)
|
|
50
|
+
pending = [root]
|
|
51
|
+
seen = {}
|
|
52
|
+
work = 0
|
|
53
|
+
until pending.empty?
|
|
54
|
+
node = name_tree_node(pending.pop, seen)
|
|
55
|
+
work = count_work(work)
|
|
56
|
+
pairs = name_pairs(node[:Names])
|
|
57
|
+
pairs.each_slice(2) do |key, value|
|
|
58
|
+
work = count_work(work)
|
|
59
|
+
return value if name_matches?(key, name)
|
|
60
|
+
end
|
|
61
|
+
pending.concat(name_children(node[:Kids]).reverse)
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def name_tree_node(raw, seen)
|
|
67
|
+
node = @document.wrap(@document.deref(raw))
|
|
68
|
+
raise LookupFailure, :malformed_name_tree unless node.value.is_a?(Hash)
|
|
69
|
+
raise LookupFailure, :destination_lookup_cycle if seen[node.value.object_id]
|
|
70
|
+
seen[node.value.object_id] = true
|
|
71
|
+
node
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def name_pairs(raw)
|
|
75
|
+
pairs = name_children(raw)
|
|
76
|
+
raise LookupFailure, :malformed_name_tree if pairs.size.odd?
|
|
77
|
+
pairs
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def name_children(raw)
|
|
81
|
+
return [] unless raw
|
|
82
|
+
array = @document.wrap(@document.deref(raw))
|
|
83
|
+
raise LookupFailure, :malformed_name_tree unless array.value.is_a?(Array)
|
|
84
|
+
array.to_a
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def count_work(work)
|
|
88
|
+
raise LookupFailure, :destination_lookup_limit if work >= @limit
|
|
89
|
+
work + 1
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
class OutlineFacts
|
|
7
|
+
LOOKUP_FAILURES = [:destination_lookup_cycle, :destination_lookup_limit, :malformed_name_tree].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(document, pages, limits)
|
|
10
|
+
@document = document
|
|
11
|
+
@destinations = DestinationResolver.new(document, pages, limits)
|
|
12
|
+
@limit = limits.outline_entries
|
|
13
|
+
@entries = []
|
|
14
|
+
@reason = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
read_outline
|
|
19
|
+
publish_outline
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def read_outline
|
|
25
|
+
root = @document.catalog[:Outlines]
|
|
26
|
+
return if root.nil?
|
|
27
|
+
root = outline_node(root)
|
|
28
|
+
validate_children(root)
|
|
29
|
+
traverse(root)
|
|
30
|
+
rescue HexaPDF::Error
|
|
31
|
+
degrade(:malformed_outline)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def traverse(root)
|
|
35
|
+
pending = [[root[:First], nil, root[:Last]]]
|
|
36
|
+
seen = {}
|
|
37
|
+
until pending.empty?
|
|
38
|
+
raw, parent_id, last = pending.pop
|
|
39
|
+
next if raw.nil?
|
|
40
|
+
item = outline_node(raw)
|
|
41
|
+
return degrade(:cycle_detected) if seen[item.value.object_id]
|
|
42
|
+
return degrade(:outline_entries_limit) if @entries.size >= @limit
|
|
43
|
+
seen[item.value.object_id] = true
|
|
44
|
+
entry = append_entry(item, parent_id)
|
|
45
|
+
schedule_links(pending, item, entry, last)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def outline_node(raw)
|
|
50
|
+
node = @document.deref(raw)
|
|
51
|
+
node = @document.wrap(node) unless node.is_a?(HexaPDF::Object)
|
|
52
|
+
raise HexaPDF::Error unless node.value.is_a?(Hash)
|
|
53
|
+
node
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def append_entry(item, parent_id)
|
|
57
|
+
Entry.new(id: @entries.size, parent_id: parent_id,
|
|
58
|
+
title: outline_title(item), destination: destination(item)).tap { |entry| @entries << entry }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def outline_title(item)
|
|
62
|
+
title = item[:Title]
|
|
63
|
+
title = title.to_s if title.is_a?(Symbol)
|
|
64
|
+
raise HexaPDF::Error unless title.is_a?(String)
|
|
65
|
+
title = title.encode(Encoding::UTF_8)
|
|
66
|
+
raise HexaPDF::Error unless title.valid_encoding? && !title.include?("\0")
|
|
67
|
+
title.dup.freeze
|
|
68
|
+
rescue EncodingError
|
|
69
|
+
raise HexaPDF::Error
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def destination(item)
|
|
73
|
+
fact = @destinations.resolve(item, prior_reason: @reason)
|
|
74
|
+
degrade(fact.reason) if LOOKUP_FAILURES.include?(fact.reason)
|
|
75
|
+
fact
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def schedule_links(pending, item, entry, last)
|
|
79
|
+
validate_children(item)
|
|
80
|
+
following = item[:Next]
|
|
81
|
+
outline_node(following) unless following.nil?
|
|
82
|
+
validate_last(item, following, last) unless last.nil?
|
|
83
|
+
pending << [following, entry.parent_id, last]
|
|
84
|
+
pending << [item[:First], entry.id, item[:Last]]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_children(node)
|
|
88
|
+
first, last, count = node[:First], node[:Last], node[:Count]
|
|
89
|
+
[first, last].each { |link| outline_node(link) unless link.nil? }
|
|
90
|
+
raise HexaPDF::Error unless count.nil? || count.is_a?(Integer)
|
|
91
|
+
raise HexaPDF::Error if first.nil? && (!last.nil? || (count && count != 0))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_last(item, following, last)
|
|
95
|
+
is_last = item.value.equal?(outline_node(last).value)
|
|
96
|
+
raise HexaPDF::Error if following.nil? != is_last
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def publish_outline
|
|
100
|
+
status = if @reason
|
|
101
|
+
@entries.empty? ? :unavailable : :incomplete
|
|
102
|
+
else
|
|
103
|
+
:complete
|
|
104
|
+
end
|
|
105
|
+
Outline.new(entries: @entries.freeze, status: status, reason: @reason)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def degrade(reason)
|
|
109
|
+
@reason ||= reason
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Mediums
|
|
7
|
+
module Pdf
|
|
8
|
+
class OutlineParser
|
|
9
|
+
def initialize(reader, source_ref)
|
|
10
|
+
@reader = reader
|
|
11
|
+
@source_ref = source_ref
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def chapters
|
|
15
|
+
items = extract_items
|
|
16
|
+
items.each_with_index.map { |item, index| to_chapter(item, index, items) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :reader, :source_ref
|
|
22
|
+
|
|
23
|
+
def extract_items
|
|
24
|
+
reader.outline_items.each_with_object([]) do |(item, _level), entries|
|
|
25
|
+
entry = parse_item(item)
|
|
26
|
+
entries << entry if entry
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_item(item)
|
|
31
|
+
title = item[:Title]
|
|
32
|
+
page = resolve_page(item)
|
|
33
|
+
return nil if title.nil? || title.empty? || page.nil?
|
|
34
|
+
|
|
35
|
+
{id: chapter_id(title, page), title: title, page: page}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve_page(item)
|
|
39
|
+
dest = item.destination || goto_destination(item)
|
|
40
|
+
return nil unless dest
|
|
41
|
+
|
|
42
|
+
page_obj = dest.first
|
|
43
|
+
reader.page_index(page_obj)&.succ
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def goto_destination(item)
|
|
47
|
+
action = item[:A]
|
|
48
|
+
return nil unless action
|
|
49
|
+
|
|
50
|
+
action[:D] if action[:S] == :GoTo
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def chapter_id(title, page)
|
|
54
|
+
Digest::SHA256.hexdigest("#{page}:#{title}")[0, 8]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_chapter(item, index, all_items)
|
|
58
|
+
start_page = item[:page]
|
|
59
|
+
next_start_page = all_items.dig(index + 1, :page)
|
|
60
|
+
last_page = next_start_page ? [next_start_page.pred, start_page].max : reader.page_count
|
|
61
|
+
|
|
62
|
+
Chapter.new(source_ref: source_ref, id: item[:id], title: item[:title], pages: start_page..last_page)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Substrate
|
|
4
|
+
module Mediums
|
|
5
|
+
module Pdf
|
|
6
|
+
class PageExtractor
|
|
7
|
+
def initialize(reader)
|
|
8
|
+
@reader = reader
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def text_for_page(number)
|
|
12
|
+
page = reader.pages[number - 1]
|
|
13
|
+
return "" unless page
|
|
14
|
+
|
|
15
|
+
TextExtractor.extract(page)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def text_for_pages(range)
|
|
19
|
+
range.map { |n| text_for_page(n) }.reject(&:empty?).join("\n\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :reader
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hexapdf"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Mediums
|
|
7
|
+
module Pdf
|
|
8
|
+
class Reader
|
|
9
|
+
def initialize(path, document_class: HexaPDF::Document)
|
|
10
|
+
@path = path
|
|
11
|
+
@document_class = document_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pages
|
|
15
|
+
document.pages
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def page_count
|
|
19
|
+
pages.count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def outline_items
|
|
23
|
+
document.outline.each_item
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def page_index(page_object)
|
|
27
|
+
pages.find_index { |page| page == page_object }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :path, :document_class
|
|
33
|
+
|
|
34
|
+
def document
|
|
35
|
+
@document ||= document_class.open(path, config: {"filter.map" => {BrotliDecode: nil}})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hexapdf"
|
|
4
|
+
|
|
5
|
+
module Substrate
|
|
6
|
+
module Mediums
|
|
7
|
+
module Pdf
|
|
8
|
+
class TextExtractor < HexaPDF::Content::Processor
|
|
9
|
+
def self.extract(page)
|
|
10
|
+
new(page.resources).tap { |extractor| page.process_contents(extractor) }.text
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :text
|
|
14
|
+
|
|
15
|
+
def initialize(resources)
|
|
16
|
+
super
|
|
17
|
+
@text = +""
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show_text(str)
|
|
21
|
+
append(decode_text(str))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def show_text_with_positioning(arr)
|
|
25
|
+
append(decode_text(arr))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def append(str)
|
|
31
|
+
text << str << " "
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -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
|