bynn-decisionnode 0.4.1
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/CHANGELOG.md +128 -0
- data/LICENSE +21 -0
- data/README.md +908 -0
- data/lib/decisionnode/batches.rb +246 -0
- data/lib/decisionnode/boosters.rb +692 -0
- data/lib/decisionnode/checks.rb +722 -0
- data/lib/decisionnode/client.rb +1100 -0
- data/lib/decisionnode/collections.rb +535 -0
- data/lib/decisionnode/document.rb +179 -0
- data/lib/decisionnode/errors.rb +138 -0
- data/lib/decisionnode/image.rb +194 -0
- data/lib/decisionnode/image_header.rb +121 -0
- data/lib/decisionnode/keys.rb +383 -0
- data/lib/decisionnode/questions.rb +135 -0
- data/lib/decisionnode/results.rb +539 -0
- data/lib/decisionnode/session.rb +286 -0
- data/lib/decisionnode/session_stream.rb +255 -0
- data/lib/decisionnode/supervisor.rb +107 -0
- data/lib/decisionnode/transport.rb +249 -0
- data/lib/decisionnode/uploads.rb +422 -0
- data/lib/decisionnode/version.rb +9 -0
- data/lib/decisionnode/video.rb +128 -0
- data/lib/decisionnode.rb +39 -0
- metadata +85 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DecisionNode
|
|
4
|
+
# A booster named with settings, for the `boosters:` argument of #decide,
|
|
5
|
+
# the helpers, #decide_many requests and batch requests. A booster reads the
|
|
6
|
+
# request's images before the decision; its reading comes back in
|
|
7
|
+
# Decision#evidence and the model sees it as evidence.
|
|
8
|
+
#
|
|
9
|
+
# DecisionNode::Booster.new("ai_generated", images: ["c3d4"], require: true)
|
|
10
|
+
#
|
|
11
|
+
# name:: the booster's name, for example "age" or "ai_generated".
|
|
12
|
+
# images:: the ids of the request's images it reads; left out, every image.
|
|
13
|
+
# require:: true: a booster that fails or does not answer in time fails the
|
|
14
|
+
# request with the 529 `booster_unavailable` (an OverloadedError,
|
|
15
|
+
# retried). Left out or false: the decision runs without it.
|
|
16
|
+
# options:: the booster's settings, a Hash.
|
|
17
|
+
#
|
|
18
|
+
# A plain name ("age") or a Hash in the API's wire form is accepted wherever
|
|
19
|
+
# a Booster is. Nothing about boosters is checked before sending: the API
|
|
20
|
+
# answers a malformed list, an unknown name and a booster turned off.
|
|
21
|
+
class Booster
|
|
22
|
+
attr_reader :name, :images, :options
|
|
23
|
+
|
|
24
|
+
def initialize(name, images: nil, require: nil, options: nil)
|
|
25
|
+
@name = name.is_a?(Symbol) ? name.to_s : name
|
|
26
|
+
@images = images.is_a?(Array) ? images.dup.freeze : images
|
|
27
|
+
@require = require
|
|
28
|
+
@options = options.is_a?(Hash) ? options.dup.freeze : options
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether a failed booster fails the request, as given (nil when not given).
|
|
32
|
+
def require
|
|
33
|
+
@require
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The booster's object form: `images`, `require` and `options` only when given.
|
|
37
|
+
def to_h
|
|
38
|
+
wire = { "name" => name }
|
|
39
|
+
wire["images"] = images unless images.nil?
|
|
40
|
+
wire["require"] = @require unless @require.nil?
|
|
41
|
+
wire["options"] = options unless options.nil?
|
|
42
|
+
wire
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The booster as it is sent: its name alone when no setting is given, so
|
|
46
|
+
# a list of such boosters is the plain list of names; else #to_h.
|
|
47
|
+
def wire
|
|
48
|
+
images.nil? && @require.nil? && options.nil? ? name : to_h
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The wire form as JSON, wherever the Booster sits in a value.
|
|
52
|
+
def to_json(*args)
|
|
53
|
+
wire.to_json(*args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect
|
|
57
|
+
"#<#{self.class.name} #{name.inspect}>"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
KNOWLEDGE_OPTIONS = %i[query scope match filter top_k max_tokens pinned min_score include_text].freeze
|
|
61
|
+
private_constant :KNOWLEDGE_OPTIONS
|
|
62
|
+
|
|
63
|
+
# The `knowledge` booster, reading the passages of one knowledge base that
|
|
64
|
+
# apply to the request:
|
|
65
|
+
#
|
|
66
|
+
# DecisionNode.knowledge("col_...", scope: "M-42", require: true)
|
|
67
|
+
#
|
|
68
|
+
# collection:: the knowledge base's `col_` id (required).
|
|
69
|
+
# query:: replaces the query built from the questions and the state.
|
|
70
|
+
# scope:: the value of the knowledge base's scope key.
|
|
71
|
+
# match, filter:: metadata conditions, JSON values sent as given.
|
|
72
|
+
# top_k:: the most retrieved passages, pins not counted; 0 reads the pins only.
|
|
73
|
+
# max_tokens:: the passages' token budget, an Integer or "auto".
|
|
74
|
+
# pinned:: false leaves the pinned documents out.
|
|
75
|
+
# min_score:: a similarity floor for passages found by meaning alone.
|
|
76
|
+
# include_text:: true adds each passage's text to the reading (a full-scope key).
|
|
77
|
+
# require:: true: a reading that did not run fails the request.
|
|
78
|
+
#
|
|
79
|
+
# Each option is sent only when given, as given; none is checked here.
|
|
80
|
+
def self.knowledge(collection, require: false, **options)
|
|
81
|
+
unknown = options.keys - KNOWLEDGE_OPTIONS
|
|
82
|
+
raise ArgumentError, "unknown knowledge option(s) #{unknown.join(', ')}" unless unknown.empty?
|
|
83
|
+
|
|
84
|
+
wire = { "collection" => collection }
|
|
85
|
+
KNOWLEDGE_OPTIONS.each { |k| wire[k.to_s] = options[k] unless options[k].nil? }
|
|
86
|
+
new("knowledge", require: require == true ? true : nil, options: wire)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class << self
|
|
91
|
+
# See Booster.knowledge.
|
|
92
|
+
def knowledge(collection, require: false, **options)
|
|
93
|
+
Booster.knowledge(collection, require: require, **options)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The status and time of one booster on a request, from the
|
|
98
|
+
# `x-decisionnode-boosters` response header: `status` ("ok", "timeout",
|
|
99
|
+
# "error", "skipped", or another value as sent) and `latency_ms` (an Integer,
|
|
100
|
+
# or nil when the header gave none).
|
|
101
|
+
BoosterStatus = Struct.new(:status, :latency_ms) do
|
|
102
|
+
# The header's value as booster name to BoosterStatus, in the header's
|
|
103
|
+
# order; an empty Hash when there is no header. An entry it cannot read is
|
|
104
|
+
# left out; it never raises.
|
|
105
|
+
def self.parse(value)
|
|
106
|
+
out = {}
|
|
107
|
+
return out unless value.is_a?(String)
|
|
108
|
+
|
|
109
|
+
value.split(",").each do |item|
|
|
110
|
+
name, eq, rest = item.strip.partition("=")
|
|
111
|
+
name = name.strip
|
|
112
|
+
next if eq.empty? || name.empty?
|
|
113
|
+
|
|
114
|
+
status, _, ms = rest.partition(":")
|
|
115
|
+
ms = ms.strip
|
|
116
|
+
out[name] = new(status.strip, ms.match?(/\A[0-9]+\z/) ? Integer(ms, 10) : nil)
|
|
117
|
+
end
|
|
118
|
+
out
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# What one booster found on a request, an entry of Decision#evidence.
|
|
123
|
+
class Evidence
|
|
124
|
+
# The version of the booster that read, for example "age-v3".
|
|
125
|
+
attr_reader :version
|
|
126
|
+
# "ok", "timeout" (no answer in time), "error", "skipped" (none of the
|
|
127
|
+
# request's images is one it reads), or another value as sent.
|
|
128
|
+
attr_reader :status
|
|
129
|
+
# How long the booster took, in milliseconds, as sent.
|
|
130
|
+
attr_reader :latency_ms
|
|
131
|
+
# The calls of this booster billed on this request, or nil when the server
|
|
132
|
+
# did not send it.
|
|
133
|
+
attr_reader :billed_calls
|
|
134
|
+
# One reading per image the booster was to read: an AgeReading, an
|
|
135
|
+
# AiGeneratedReading or an UnknownReading. A picture it did not read (it
|
|
136
|
+
# timed out or failed) or could not decode (`readable` false) is an
|
|
137
|
+
# UnknownReading holding those fields; `status` says why.
|
|
138
|
+
attr_reader :results
|
|
139
|
+
# The entry as received.
|
|
140
|
+
attr_reader :raw
|
|
141
|
+
|
|
142
|
+
# The `evidence` of a response body as booster name to Evidence, in the
|
|
143
|
+
# body's order, or nil when the body has none.
|
|
144
|
+
def self.map(evidence)
|
|
145
|
+
return nil unless evidence.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
evidence.each_with_object({}) { |(name, entry), h| h[name] = new(name, entry) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def initialize(booster, raw)
|
|
151
|
+
@raw = raw
|
|
152
|
+
entry = raw.is_a?(Hash) ? raw : {}
|
|
153
|
+
@version = entry["version"].is_a?(String) ? entry["version"] : nil
|
|
154
|
+
@status = entry["status"].is_a?(String) ? entry["status"] : nil
|
|
155
|
+
@latency_ms = entry["latency_ms"].is_a?(Numeric) ? entry["latency_ms"] : nil
|
|
156
|
+
@billed_calls = entry["billed_calls"].is_a?(Integer) ? entry["billed_calls"] : nil
|
|
157
|
+
results = entry["results"].is_a?(Array) ? entry["results"] : []
|
|
158
|
+
@results = results.map { |r| Reading.from(booster, r) }.freeze
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_h
|
|
162
|
+
raw
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The base of every booster reading. `raw` holds every field the server sent.
|
|
167
|
+
class Reading
|
|
168
|
+
# The id of the image read, or nil when the server sent none.
|
|
169
|
+
attr_reader :image
|
|
170
|
+
attr_reader :raw
|
|
171
|
+
|
|
172
|
+
# The typed reading for a booster this gem knows, when every field is
|
|
173
|
+
# present and of the right type; an UnknownReading holding the raw fields
|
|
174
|
+
# otherwise, so a new booster or a malformed reading never fails the decision.
|
|
175
|
+
def self.from(booster, raw)
|
|
176
|
+
klass = KINDS[booster]
|
|
177
|
+
return UnknownReading.new({ "value" => raw }) unless raw.is_a?(Hash)
|
|
178
|
+
|
|
179
|
+
klass&.well_formed?(raw) ? klass.new(raw) : UnknownReading.new(raw)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.number?(value)
|
|
183
|
+
value.is_a?(Numeric)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A whole number of 0 or more: a grid's rows and columns, a picture's sides.
|
|
187
|
+
def self.count?(value)
|
|
188
|
+
value.is_a?(Integer) && !value.negative?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Absent (nil) or of the class given.
|
|
192
|
+
def self.optional?(value, klass)
|
|
193
|
+
value.nil? || value.is_a?(klass)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def initialize(raw)
|
|
197
|
+
@raw = raw
|
|
198
|
+
@image = raw.is_a?(Hash) && raw["image"].is_a?(String) ? raw["image"] : nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A field of the reading as the server sent it.
|
|
202
|
+
def [](key)
|
|
203
|
+
raw.is_a?(Hash) ? raw[key.to_s] : nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def to_h
|
|
207
|
+
raw
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# A reading of a booster this gem does not know, or one with a field
|
|
212
|
+
# missing or of the wrong type. `raw` holds it as received (a reading that
|
|
213
|
+
# is not an object as `{"value" => reading}`).
|
|
214
|
+
class UnknownReading < Reading; end
|
|
215
|
+
|
|
216
|
+
# A reading of the `age` booster: the age of the largest face in the image.
|
|
217
|
+
# With `faces` 0 the image shows no face and the age fields are nil.
|
|
218
|
+
class AgeReading < Reading
|
|
219
|
+
INTEGER_FIELDS = %w[age_years low high].freeze
|
|
220
|
+
private_constant :INTEGER_FIELDS
|
|
221
|
+
|
|
222
|
+
# How many faces were found; the largest is read.
|
|
223
|
+
attr_reader :faces
|
|
224
|
+
# The age in years, and the range `low` to `high`.
|
|
225
|
+
attr_reader :age_years, :low, :high
|
|
226
|
+
# From 0 to 1.
|
|
227
|
+
attr_reader :confidence
|
|
228
|
+
|
|
229
|
+
def self.well_formed?(raw)
|
|
230
|
+
faces = raw["faces"]
|
|
231
|
+
return false unless raw["image"].is_a?(String) && faces.is_a?(Integer) && !faces.negative?
|
|
232
|
+
|
|
233
|
+
# With no face the age fields are left out; with one, each is required.
|
|
234
|
+
INTEGER_FIELDS.all? { |k| raw[k].is_a?(Integer) || (raw[k].nil? && faces.zero?) } &&
|
|
235
|
+
(number?(raw["confidence"]) || (raw["confidence"].nil? && faces.zero?))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def initialize(raw)
|
|
239
|
+
super
|
|
240
|
+
@faces = raw["faces"]
|
|
241
|
+
@age_years = raw["age_years"]
|
|
242
|
+
@low = raw["low"]
|
|
243
|
+
@high = raw["high"]
|
|
244
|
+
@confidence = raw["confidence"]
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# The likeliest generator of an AiGeneratedReading: its `name` and `probability`.
|
|
249
|
+
TopGenerator = Struct.new(:name, :probability)
|
|
250
|
+
|
|
251
|
+
# A reading of the `ai_generated` booster: how likely the image was made by
|
|
252
|
+
# an image generator, and who signed its Content Credentials (C2PA) if it
|
|
253
|
+
# carries any.
|
|
254
|
+
class AiGeneratedReading < Reading
|
|
255
|
+
# The probability that the image was made by a generator, from 0 to 1.
|
|
256
|
+
attr_reader :generated
|
|
257
|
+
# From 0 to 1.
|
|
258
|
+
attr_reader :confidence
|
|
259
|
+
# "inference" (read from the pixels), "c2pa" (the file's Content
|
|
260
|
+
# Credentials say so), or another value as sent.
|
|
261
|
+
attr_reader :method
|
|
262
|
+
# The likeliest generator, a TopGenerator, when the reading says the image
|
|
263
|
+
# is likely generated; nil otherwise, and always on a "c2pa" reading.
|
|
264
|
+
attr_reader :top_generator
|
|
265
|
+
# Who signed the file's Content Credentials when they are trusted; nil
|
|
266
|
+
# otherwise.
|
|
267
|
+
attr_reader :c2pa_signer
|
|
268
|
+
# "untrusted" when the file carries Content Credentials that validate but
|
|
269
|
+
# are not trusted (they name no signer and change nothing in the reading),
|
|
270
|
+
# another value as sent, or nil when the reading has none.
|
|
271
|
+
attr_reader :c2pa
|
|
272
|
+
|
|
273
|
+
def self.well_formed?(raw)
|
|
274
|
+
top = raw["top_generator"]
|
|
275
|
+
raw["image"].is_a?(String) && number?(raw["generated"]) && number?(raw["confidence"]) &&
|
|
276
|
+
raw["method"].is_a?(String) &&
|
|
277
|
+
# top_generator and c2pa_signer are always sent, null when unset; c2pa
|
|
278
|
+
# only when the file's Content Credentials are not trusted.
|
|
279
|
+
raw.key?("top_generator") && raw.key?("c2pa_signer") &&
|
|
280
|
+
(top.nil? || (top.is_a?(Hash) && top["name"].is_a?(String) && number?(top["probability"]))) &&
|
|
281
|
+
(raw["c2pa_signer"].nil? || raw["c2pa_signer"].is_a?(String)) &&
|
|
282
|
+
(raw["c2pa"].nil? || raw["c2pa"].is_a?(String))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def initialize(raw)
|
|
286
|
+
super
|
|
287
|
+
@generated = raw["generated"]
|
|
288
|
+
@confidence = raw["confidence"]
|
|
289
|
+
@method = raw["method"]
|
|
290
|
+
top = raw["top_generator"]
|
|
291
|
+
@top_generator = top && TopGenerator.new(top["name"], top["probability"])
|
|
292
|
+
@c2pa_signer = raw["c2pa_signer"]
|
|
293
|
+
@c2pa = raw["c2pa"]
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# A reading of the `ocr` booster: the text the image shows, in reading
|
|
298
|
+
# order, or the answer to the booster's `prompt` option.
|
|
299
|
+
class OcrReading < Reading
|
|
300
|
+
# The text read; empty when the image shows none, which is a reading, not
|
|
301
|
+
# a failure.
|
|
302
|
+
attr_reader :text
|
|
303
|
+
# true when the reading was cut at the model's context limit.
|
|
304
|
+
attr_reader :truncated
|
|
305
|
+
|
|
306
|
+
def self.well_formed?(raw)
|
|
307
|
+
raw["image"].is_a?(String) && raw["text"].is_a?(String) &&
|
|
308
|
+
[true, false].include?(raw["truncated"])
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def initialize(raw)
|
|
312
|
+
super
|
|
313
|
+
@text = raw["text"]
|
|
314
|
+
@truncated = raw["truncated"]
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# A face's box: its corners `x1`, `y1` (top left) and `x2`, `y2` (bottom
|
|
319
|
+
# right), the numbers as sent: pixels of the image, or fractions of its
|
|
320
|
+
# width and height when every place question of the request asks for
|
|
321
|
+
# normalized units.
|
|
322
|
+
FaceBox = Struct.new(:x1, :y1, :x2, :y2) do
|
|
323
|
+
def self.well_formed?(raw)
|
|
324
|
+
raw.is_a?(Hash) && %w[x1 y1 x2 y2].all? { |k| raw[k].is_a?(Numeric) }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def self.from(raw)
|
|
328
|
+
new(raw["x1"], raw["y1"], raw["x2"], raw["y2"])
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# One face a `face_detection` reading found: its `box` (a FaceBox) and its
|
|
333
|
+
# `confidence`, from 0 to 1.
|
|
334
|
+
FaceDetection = Struct.new(:box, :confidence)
|
|
335
|
+
|
|
336
|
+
# One face a `face_recognition` reading matched in a collection: its `box`
|
|
337
|
+
# (a FaceBox), the match `score` from 0 to 1, the `collection` searched, the
|
|
338
|
+
# matched item's `reference`, and its `name` (a public figure) or `label`
|
|
339
|
+
# (an item of your own collection that has one), each nil when absent.
|
|
340
|
+
# `collection_name` is an own collection's display name (or its id), or a
|
|
341
|
+
# public collection's id; `metadata` is an own item's metadata, a Hash of
|
|
342
|
+
# JSON values as sent. Each is nil when absent, never an empty Hash.
|
|
343
|
+
FaceMatch = Struct.new(:box, :score, :collection, :reference, :name, :label, :collection_name, :metadata)
|
|
344
|
+
|
|
345
|
+
# The faces of a face reading, checked alike for both boosters.
|
|
346
|
+
module FaceReadings
|
|
347
|
+
module_function
|
|
348
|
+
|
|
349
|
+
def counted?(raw)
|
|
350
|
+
raw["image"].is_a?(String) && raw["faces"].is_a?(Integer) && !raw["faces"].negative?
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def list?(raw, key)
|
|
354
|
+
raw[key].is_a?(Array) && raw[key].all? { |entry| entry.is_a?(Hash) && FaceBox.well_formed?(entry["box"]) && yield(entry) }
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
private_constant :FaceReadings
|
|
358
|
+
|
|
359
|
+
# A reading of the `face_detection` booster: how many faces the image
|
|
360
|
+
# shows, and each face's box and confidence.
|
|
361
|
+
class FaceDetectionReading < Reading
|
|
362
|
+
# How many faces were found; it can be more than the faces listed, which
|
|
363
|
+
# are at most 64.
|
|
364
|
+
attr_reader :faces
|
|
365
|
+
# The faces found, in the order sent: each a FaceDetection.
|
|
366
|
+
attr_reader :detections
|
|
367
|
+
|
|
368
|
+
def self.well_formed?(raw)
|
|
369
|
+
FaceReadings.counted?(raw) && FaceReadings.list?(raw, "detections") { |d| number?(d["confidence"]) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def initialize(raw)
|
|
373
|
+
super
|
|
374
|
+
@faces = raw["faces"]
|
|
375
|
+
@detections = raw["detections"].map { |d| FaceDetection.new(FaceBox.from(d["box"]), d["confidence"]) }.freeze
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# A reading of the `face_recognition` booster: how many faces it searched,
|
|
380
|
+
# and each face that matched an item of the collection, by score.
|
|
381
|
+
class FaceRecognitionReading < Reading
|
|
382
|
+
# How many faces were searched.
|
|
383
|
+
attr_reader :faces
|
|
384
|
+
# An own collection's display name, or nil when absent (a public
|
|
385
|
+
# collection, or an answer from before the field).
|
|
386
|
+
attr_reader :collection_name
|
|
387
|
+
# The faces that matched, in the order sent: each a FaceMatch. Empty when
|
|
388
|
+
# none did.
|
|
389
|
+
attr_reader :matches
|
|
390
|
+
|
|
391
|
+
def self.well_formed?(raw)
|
|
392
|
+
FaceReadings.counted?(raw) &&
|
|
393
|
+
(raw["collection_name"].nil? || raw["collection_name"].is_a?(String)) &&
|
|
394
|
+
FaceReadings.list?(raw, "matches") do |m|
|
|
395
|
+
number?(m["score"]) && m["collection"].is_a?(String) && m["reference"].is_a?(String) &&
|
|
396
|
+
%w[name label collection_name].all? { |k| m[k].nil? || m[k].is_a?(String) } &&
|
|
397
|
+
(m["metadata"].nil? || m["metadata"].is_a?(Hash))
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def initialize(raw)
|
|
402
|
+
super
|
|
403
|
+
@faces = raw["faces"]
|
|
404
|
+
@collection_name = raw["collection_name"]
|
|
405
|
+
@matches = raw["matches"].map do |m|
|
|
406
|
+
FaceMatch.new(FaceBox.from(m["box"]), m["score"], m["collection"], m["reference"], m["name"], m["label"],
|
|
407
|
+
m["collection_name"], m["metadata"])
|
|
408
|
+
end.freeze
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# A reading of the `bynn_depth` booster (Depth Vision): how far away things
|
|
413
|
+
# are in the picture, in meters. The distances are estimates from one
|
|
414
|
+
# picture, for decisions and planning, never a safety stop.
|
|
415
|
+
class DepthReading < Reading
|
|
416
|
+
# Meters to the nearest obstacle straight ahead (the middle third of the
|
|
417
|
+
# picture, the floor or ground left out); nil when none is found.
|
|
418
|
+
attr_reader :ahead
|
|
419
|
+
# The same in 7 equal vertical strips of the picture, left to right, each
|
|
420
|
+
# meters or nil; a reading with another number of them is an
|
|
421
|
+
# UnknownReading.
|
|
422
|
+
attr_reader :directions
|
|
423
|
+
# true when a floor or ground was found and left out of `ahead` and
|
|
424
|
+
# `directions`; false when none was, and they include everything.
|
|
425
|
+
attr_reader :ground
|
|
426
|
+
# The grid's rows and columns.
|
|
427
|
+
attr_reader :grid_rows, :grid_cols
|
|
428
|
+
# The grid, row by row from the top left of the picture turned upright,
|
|
429
|
+
# each cell the distance of its part of the picture in meters.
|
|
430
|
+
attr_reader :grid
|
|
431
|
+
# The nearest, the farthest and the median distance in the picture.
|
|
432
|
+
attr_reader :depth_min, :depth_max, :depth_median
|
|
433
|
+
# The picture's size in pixels, turned upright.
|
|
434
|
+
attr_reader :width, :height
|
|
435
|
+
|
|
436
|
+
# A reading's directions: 7 equal vertical strips of the picture.
|
|
437
|
+
DIRECTIONS = 7
|
|
438
|
+
|
|
439
|
+
def self.well_formed?(raw)
|
|
440
|
+
raw["image"].is_a?(String) && (raw["ahead"].nil? || number?(raw["ahead"])) &&
|
|
441
|
+
raw["directions"].is_a?(Array) && raw["directions"].size == DIRECTIONS &&
|
|
442
|
+
raw["directions"].all? { |d| d.nil? || number?(d) } &&
|
|
443
|
+
[true, false].include?(raw["ground"]) &&
|
|
444
|
+
count?(raw["grid_rows"]) && count?(raw["grid_cols"]) &&
|
|
445
|
+
raw["grid"].is_a?(Array) && raw["grid"].all? { |row| row.is_a?(Array) && row.all? { |c| number?(c) } } &&
|
|
446
|
+
%w[depth_min depth_max depth_median].all? { |k| number?(raw[k]) } &&
|
|
447
|
+
count?(raw["width"]) && count?(raw["height"])
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def initialize(raw)
|
|
451
|
+
super
|
|
452
|
+
@ahead = raw["ahead"]
|
|
453
|
+
@directions = raw["directions"]
|
|
454
|
+
@ground = raw["ground"]
|
|
455
|
+
@grid_rows = raw["grid_rows"]
|
|
456
|
+
@grid_cols = raw["grid_cols"]
|
|
457
|
+
@grid = raw["grid"]
|
|
458
|
+
@depth_min = raw["depth_min"]
|
|
459
|
+
@depth_max = raw["depth_max"]
|
|
460
|
+
@depth_median = raw["depth_median"]
|
|
461
|
+
@width = raw["width"]
|
|
462
|
+
@height = raw["height"]
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# The opening a `bynn_segment` instance encloses, such as the hole of a
|
|
467
|
+
# gate, a doorway or a ring: its `polygon` (`[x, y]` points), `centroid`
|
|
468
|
+
# (`[x, y]`), `width` and `height`, `offset` (`[dx, dy]`, -1 to 1) and
|
|
469
|
+
# `area`, as fractions of the picture from its top left.
|
|
470
|
+
SegmentOpening = Struct.new(:polygon, :centroid, :width, :height, :offset, :area)
|
|
471
|
+
|
|
472
|
+
# How far away a `bynn_segment` instance is, with depth on: the `median`
|
|
473
|
+
# distance inside its outline, the `opening_median` seen through its
|
|
474
|
+
# opening and the `opening_rim_median` of the opening's rim, each meters or
|
|
475
|
+
# nil.
|
|
476
|
+
SegmentDepth = Struct.new(:median, :opening_median, :opening_rim_median)
|
|
477
|
+
|
|
478
|
+
# A `width` and a `height` in meters.
|
|
479
|
+
SegmentExtent = Struct.new(:width, :height)
|
|
480
|
+
|
|
481
|
+
# How large a `bynn_segment` instance is, with depth on: its `width` and
|
|
482
|
+
# `height` in meters, and its `opening`'s, a SegmentExtent or nil.
|
|
483
|
+
SegmentSize = Struct.new(:width, :height, :opening)
|
|
484
|
+
|
|
485
|
+
# One thing a `bynn_segment` reading found: the phrase it matched
|
|
486
|
+
# (`label`), its `score` from 0 to 1, `box` (`[x1, y1, x2, y2]`), `polygon`
|
|
487
|
+
# (its outline, `[x, y]` points), `area` and `centroid` (`[x, y]`), as
|
|
488
|
+
# fractions of the picture from its top left, turned upright; its
|
|
489
|
+
# `opening` (a SegmentOpening or nil), and with depth on its `depth` (a
|
|
490
|
+
# SegmentDepth) and `size_m` (a SegmentSize), each nil with depth off.
|
|
491
|
+
SegmentInstance = Struct.new(:label, :score, :box, :polygon, :area, :centroid, :opening, :depth, :size_m)
|
|
492
|
+
|
|
493
|
+
# The shapes of a segment reading, checked as sent.
|
|
494
|
+
module SegmentShapes
|
|
495
|
+
module_function
|
|
496
|
+
|
|
497
|
+
def numbers?(value, count = nil)
|
|
498
|
+
value.is_a?(Array) && value.all? { |v| v.is_a?(Numeric) } && (count.nil? || value.size == count)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def points?(value)
|
|
502
|
+
value.is_a?(Array) && value.all? { |p| numbers?(p, 2) }
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# An object, or nil or absent.
|
|
506
|
+
def object_or_nil?(raw, key)
|
|
507
|
+
raw[key].nil? || raw[key].is_a?(Hash)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# A number, or nil or absent.
|
|
511
|
+
def nullable?(raw, key)
|
|
512
|
+
raw[key].nil? || raw[key].is_a?(Numeric)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def opening?(raw)
|
|
516
|
+
raw.nil? ||
|
|
517
|
+
(points?(raw["polygon"]) && numbers?(raw["centroid"], 2) && numbers?(raw["offset"], 2) &&
|
|
518
|
+
%w[width height area].all? { |k| raw[k].is_a?(Numeric) })
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def depth?(raw)
|
|
522
|
+
raw.nil? || %w[median opening_median opening_rim_median].all? { |k| nullable?(raw, k) }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def size?(raw)
|
|
526
|
+
raw.nil? ||
|
|
527
|
+
(raw["width"].is_a?(Numeric) && raw["height"].is_a?(Numeric) && object_or_nil?(raw, "opening") &&
|
|
528
|
+
(raw["opening"].nil? || (raw["opening"]["width"].is_a?(Numeric) && raw["opening"]["height"].is_a?(Numeric))))
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def instance?(raw)
|
|
532
|
+
raw.is_a?(Hash) && raw["label"].is_a?(String) && raw["score"].is_a?(Numeric) &&
|
|
533
|
+
numbers?(raw["box"], 4) && points?(raw["polygon"]) && raw["area"].is_a?(Numeric) &&
|
|
534
|
+
numbers?(raw["centroid"], 2) &&
|
|
535
|
+
%w[opening depth size_m].all? { |k| object_or_nil?(raw, k) } &&
|
|
536
|
+
opening?(raw["opening"]) && depth?(raw["depth"]) && size?(raw["size_m"])
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def instance(raw)
|
|
540
|
+
opening = raw["opening"]
|
|
541
|
+
depth = raw["depth"]
|
|
542
|
+
size = raw["size_m"]
|
|
543
|
+
SegmentInstance.new(
|
|
544
|
+
raw["label"], raw["score"], raw["box"], raw["polygon"], raw["area"], raw["centroid"],
|
|
545
|
+
opening && SegmentOpening.new(opening["polygon"], opening["centroid"], opening["width"], opening["height"],
|
|
546
|
+
opening["offset"], opening["area"]),
|
|
547
|
+
depth && SegmentDepth.new(depth["median"], depth["opening_median"], depth["opening_rim_median"]),
|
|
548
|
+
size && SegmentSize.new(size["width"], size["height"],
|
|
549
|
+
size["opening"] && SegmentExtent.new(size["opening"]["width"], size["opening"]["height"]))
|
|
550
|
+
)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
private_constant :SegmentShapes
|
|
554
|
+
|
|
555
|
+
# A reading of the `bynn_segment` booster (Segment Vision): where the things
|
|
556
|
+
# the request's `prompt` names are in the picture.
|
|
557
|
+
class SegmentReading < Reading
|
|
558
|
+
# The picture's size in pixels, turned upright.
|
|
559
|
+
attr_reader :width, :height
|
|
560
|
+
# The phrases searched for, cleaned as the request's `prompt` is.
|
|
561
|
+
attr_reader :prompts
|
|
562
|
+
# What was found, best first: each a SegmentInstance. Empty when nothing
|
|
563
|
+
# was, which is a reading.
|
|
564
|
+
attr_reader :instances
|
|
565
|
+
# Where the distances come from ("bynn_depth"); nil with depth off.
|
|
566
|
+
attr_reader :depth_source
|
|
567
|
+
# The horizontal field of view the sizes in meters assume, in degrees; nil
|
|
568
|
+
# when the reading has none.
|
|
569
|
+
attr_reader :assumed_hfov_deg
|
|
570
|
+
|
|
571
|
+
def self.well_formed?(raw)
|
|
572
|
+
raw["image"].is_a?(String) && count?(raw["width"]) && count?(raw["height"]) &&
|
|
573
|
+
raw["prompts"].is_a?(Array) && raw["prompts"].all? { |p| p.is_a?(String) } &&
|
|
574
|
+
raw["instances"].is_a?(Array) && raw["instances"].all? { |i| SegmentShapes.instance?(i) } &&
|
|
575
|
+
(raw["depth_source"].nil? || raw["depth_source"].is_a?(String)) &&
|
|
576
|
+
(raw["assumed_hfov_deg"].nil? || number?(raw["assumed_hfov_deg"]))
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def initialize(raw)
|
|
580
|
+
super
|
|
581
|
+
@width = raw["width"]
|
|
582
|
+
@height = raw["height"]
|
|
583
|
+
@prompts = raw["prompts"]
|
|
584
|
+
@instances = raw["instances"].map { |i| SegmentShapes.instance(i) }.freeze
|
|
585
|
+
@depth_source = raw["depth_source"]
|
|
586
|
+
@assumed_hfov_deg = raw["assumed_hfov_deg"]
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# One passage of a KnowledgeReading, in the order the model saw them
|
|
591
|
+
# (pinned first). `n` is its number in the block; `item` and `chunk` cite
|
|
592
|
+
# where it comes from, and `byte_start` to `byte_end` (end exclusive) are
|
|
593
|
+
# UTF-8 byte offsets into the item's stored text, as
|
|
594
|
+
# `collections.get_item(..., include_text: true)` returns it. `reference`,
|
|
595
|
+
# `title` and `heading` are nil for a publishable key's reading. `score` is
|
|
596
|
+
# the similarity (4 decimals), nil for a pin. `rendered` is false when the
|
|
597
|
+
# passage was left out to fit, and nil on a search's passages. `text` is
|
|
598
|
+
# there only with `include_text`, and always on a search. Every value is as
|
|
599
|
+
# sent.
|
|
600
|
+
class KnowledgePassage
|
|
601
|
+
attr_reader :n, :pinned, :item, :chunk, :byte_start, :byte_end, :reference, :title, :heading, :tokens,
|
|
602
|
+
:score, :lexical, :rendered, :text, :raw
|
|
603
|
+
|
|
604
|
+
def self.well_formed?(raw)
|
|
605
|
+
raw.is_a?(Hash) && raw["n"].is_a?(Integer) && [true, false].include?(raw["pinned"]) &&
|
|
606
|
+
raw["item"].is_a?(String) && raw["chunk"].is_a?(Integer) &&
|
|
607
|
+
raw["byte_start"].is_a?(Integer) && raw["byte_end"].is_a?(Integer) &&
|
|
608
|
+
%w[reference title heading text].all? { |k| Reading.optional?(raw[k], String) } &&
|
|
609
|
+
Reading.optional?(raw["score"], Numeric) && Reading.optional?(raw["tokens"], Integer) &&
|
|
610
|
+
%w[lexical rendered].all? { |k| raw[k].nil? || [true, false].include?(raw[k]) }
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def initialize(raw)
|
|
614
|
+
@raw = raw
|
|
615
|
+
%w[n pinned item chunk byte_start byte_end reference title heading tokens score lexical rendered
|
|
616
|
+
text].each { |k| instance_variable_set(:"@#{k}", raw[k]) }
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def pinned?
|
|
620
|
+
@pinned == true
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# A field of the passage as the server sent it.
|
|
624
|
+
def [](key)
|
|
625
|
+
raw[key.to_s]
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def to_h
|
|
629
|
+
raw
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# A reading of the `knowledge` booster, and the answer of
|
|
634
|
+
# `collections.search`: the passages found in one knowledge base.
|
|
635
|
+
#
|
|
636
|
+
# `corpus` (`{"version"}`), `query` (`{"source", "tokens", "text"}`, the
|
|
637
|
+
# text only with `include_text` and on a search), `budget` (`{"mode",
|
|
638
|
+
# "tokens", "entry", "cause"}`), `scope` (`{"key", "value"}`) and `search`
|
|
639
|
+
# (`{"plan", "candidates", "fallback"}`) are Hashes as sent, nil when
|
|
640
|
+
# absent; `omitted` is the count of passages left out by cause, as sent.
|
|
641
|
+
class KnowledgeReading < Reading
|
|
642
|
+
# The knowledge base's id.
|
|
643
|
+
attr_reader :collection
|
|
644
|
+
# Its name, or nil (always for a publishable key's reading).
|
|
645
|
+
attr_reader :collection_name
|
|
646
|
+
attr_reader :corpus, :query, :budget, :scope, :search, :omitted
|
|
647
|
+
# Strings such as "scope_not_named", "over_budget", "budget_floor" and
|
|
648
|
+
# "pinned_filtered"; empty when there are none.
|
|
649
|
+
attr_reader :warnings
|
|
650
|
+
# Each a KnowledgePassage, in the order sent.
|
|
651
|
+
attr_reader :passages
|
|
652
|
+
# Items not ready yet.
|
|
653
|
+
attr_reader :pending_items
|
|
654
|
+
# true when a passage was dropped to fit the model's context; nil on a
|
|
655
|
+
# search's answer, which has none.
|
|
656
|
+
attr_reader :truncated
|
|
657
|
+
|
|
658
|
+
def self.well_formed?(raw)
|
|
659
|
+
warnings = raw["warnings"]
|
|
660
|
+
raw["collection"].is_a?(String) && raw["passages"].is_a?(Array) &&
|
|
661
|
+
raw["passages"].all? { |p| KnowledgePassage.well_formed?(p) } &&
|
|
662
|
+
optional?(raw["collection_name"], String) && optional?(raw["pending_items"], Integer) &&
|
|
663
|
+
(raw["truncated"].nil? || [true, false].include?(raw["truncated"])) &&
|
|
664
|
+
(warnings.nil? || (warnings.is_a?(Array) && warnings.all?(String))) &&
|
|
665
|
+
%w[corpus query budget scope omitted search].all? { |k| optional?(raw[k], Hash) }
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def initialize(raw)
|
|
669
|
+
super
|
|
670
|
+
@collection = raw["collection"]
|
|
671
|
+
@collection_name = raw["collection_name"]
|
|
672
|
+
%w[corpus query budget scope search omitted pending_items truncated].each do |k|
|
|
673
|
+
instance_variable_set(:"@#{k}", raw[k])
|
|
674
|
+
end
|
|
675
|
+
@warnings = raw["warnings"].is_a?(Array) ? raw["warnings"] : []
|
|
676
|
+
@passages = raw["passages"].map { |p| KnowledgePassage.new(p) }.freeze
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
class Reading
|
|
681
|
+
KINDS = {
|
|
682
|
+
"age" => AgeReading,
|
|
683
|
+
"ai_generated" => AiGeneratedReading,
|
|
684
|
+
"face_detection" => FaceDetectionReading,
|
|
685
|
+
"face_recognition" => FaceRecognitionReading,
|
|
686
|
+
"ocr" => OcrReading,
|
|
687
|
+
"bynn_depth" => DepthReading,
|
|
688
|
+
"bynn_segment" => SegmentReading,
|
|
689
|
+
"knowledge" => KnowledgeReading
|
|
690
|
+
}.freeze
|
|
691
|
+
end
|
|
692
|
+
end
|