calmlens 0.20.0 → 0.22.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.
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CalmLens
4
+ # The project's upload presets: the rules for uploads made from browsers and apps. {#ensure}
5
+ # creates or replaces presets by `key`, for applications that declare theirs in code or
6
+ # configuration. Reach it through {ServerClient#upload_presets}.
7
+ class UploadPresets < Resource
8
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
9
+ OPERATIONS = 'uploadPresets'
10
+
11
+ # Lists one page of the project's upload presets, oldest first unless `sort` says otherwise.
12
+ # @option options [String] :cursor the previous page's `nextCursor`
13
+ # @option options [Integer] :page_size 1 to 100, default 50
14
+ # @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
15
+ # with `-`; `createdAt` by default
16
+ # @return [Hash] a cursor page: items, nextCursor and hasMore.
17
+ def list(options = {})
18
+ call(options, __method__) do |api, request|
19
+ cursor_page(request('GET', project('upload-presets'), query: api, request_options: request), 'upload preset')
20
+ end
21
+ end
22
+
23
+ # Iterates every upload preset of the project, fetching pages as needed.
24
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
25
+ # @yieldparam upload_preset [Hash]
26
+ # @return [Enumerator, nil] an Enumerator when no block is given.
27
+ def each(options = {}, &block)
28
+ return enum_for(:each, options) unless block
29
+
30
+ each_item(options, __method__, method(:list), &block)
31
+ end
32
+
33
+ # Creates each preset, or replaces it with the definition sent, by `key`. Fields left out take
34
+ # their defaults; an unchanged preset keeps its revision. Every preset is checked before any is
35
+ # saved. Send each preset's `revision` (0 for one that must not exist yet) to refuse a change
36
+ # made since with `upload-preset/revision-conflict`.
37
+ # @option options [Array<Hash>] :upload_presets `[{key:, name:, revision:, access:, enabled:, ...}]`,
38
+ # at most 50
39
+ # @return [Hash] `uploadPresets`: the stored presets, in the order sent.
40
+ def ensure(options)
41
+ call(options, __method__) do |api, request|
42
+ request('PUT', project('upload-presets'), body: api, request_options: request)
43
+ end
44
+ end
45
+
46
+ # Deletes an upload preset. Browsers can no longer upload through it; assets already uploaded stay.
47
+ # @option options [String] :upload_preset_id
48
+ # @return [nil]
49
+ def delete(options)
50
+ call(options, __method__) do |api, request|
51
+ request('DELETE', "/upload-presets/#{Internal.segment(api['uploadPresetId'])}", request_options: request)
52
+ nil
53
+ end
54
+ end
55
+ end
56
+
57
+ # The project's settings, with defaults filled in. {#update} replaces them: settings left out
58
+ # return to their defaults. Send the `revision` you read to refuse a change made since with
59
+ # `project-settings/revision-conflict`. Reach it through {ServerClient#project_settings}.
60
+ class ProjectSettings < Resource
61
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
62
+ OPERATIONS = 'projectSettings'
63
+
64
+ # @return [Hash] `projectId`, `revision` and `settings`.
65
+ def get(options = {})
66
+ call(options, __method__) { |_api, request| request('GET', project('settings'), request_options: request) }
67
+ end
68
+
69
+ # Replaces the project's settings; unchanged settings keep their revision. The project's policy
70
+ # and metadata fields have their own methods.
71
+ # @option options [Integer] :revision the revision you read
72
+ # @option options [Hash] :settings the complete settings
73
+ # @option options [Numeric] :search_indexing_budget_units needed when turning on
74
+ # `aiMediaSearchEnabled`: the most indexing units you accept for existing media
75
+ # @return [Hash] `projectId`, `revision` and `settings`.
76
+ def update(options)
77
+ call(options, __method__) { |api, request| request('PUT', project('settings'), body: api, request_options: request) }
78
+ end
79
+ end
80
+
81
+ # The project's metadata fields, in display order. They share the project settings' `revision`;
82
+ # {#ensure} creates or replaces fields by ID and keeps the others. Reach it through
83
+ # {ServerClient#metadata_fields}.
84
+ class MetadataFields < Resource
85
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
86
+ OPERATIONS = 'metadataFields'
87
+
88
+ # @return [Hash] `projectId`, `revision` and `fields`.
89
+ def list(options = {})
90
+ call(options, __method__) { |_api, request| request('GET', project('metadata-fields'), request_options: request) }
91
+ end
92
+
93
+ # Creates each field, or replaces it by ID. Other fields are kept; new ones are added at the end.
94
+ # @option options [Integer] :revision the revision you read
95
+ # @option options [Array<Hash>] :fields `[{id:, title:, type:, icon:, color:, description:}]`
96
+ # @return [Hash] `projectId`, `revision` and `fields`.
97
+ def ensure(options)
98
+ call(options, __method__) { |api, request| request('PUT', project('metadata-fields'), body: api, request_options: request) }
99
+ end
100
+
101
+ # Deletes a field definition; assets keep the metadata values they hold.
102
+ # @option options [String] :field_id
103
+ # @option options [Integer] :revision the revision you read
104
+ # @return [nil]
105
+ def delete(options)
106
+ call(options, __method__) do |api, request|
107
+ request('DELETE', project("metadata-fields/#{Internal.segment(api['fieldId'])}"),
108
+ query: without(api, 'fieldId'), request_options: request)
109
+ nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -19,9 +19,9 @@ module CalmLens
19
19
  # beside a request's; Internal::OPTION_KEYS, generated from the API, has the rest.
20
20
  LOCAL_OPTION_KEYS = {
21
21
  'assets.waitForDecision' => %w[assetId timeout].freeze,
22
- 'cdn.getAssetUrl' => %w[assetIdentifier lens resource transform viewerClaims].freeze,
23
- 'cdn.serve' => %w[assetIdentifier init lens resource transform viewerClaims].freeze,
24
- 'uploadTokens.sign' => %w[expiresInSeconds externalId uploadPresetId].freeze
22
+ 'cdn.getAssetUrl' => %w[assetId externalId lens projectId resource transform viewerClaims].freeze,
23
+ 'cdn.serve' => %w[assetId externalId init lens projectId resource transform viewerClaims].freeze,
24
+ 'uploadTokens.sign' => %w[expiresIn externalId uploadPresetId].freeze
25
25
  }.freeze
26
26
  private_constant :LOCAL_OPTION_KEYS
27
27
 
@@ -30,9 +30,10 @@ module CalmLens
30
30
  # `sdk/invalid-argument` instead of being silently dropped.
31
31
  # An option given as nil is left out, as if omitted, unless the API takes null for it
32
32
  # (Internal::NULLABLE_OPTION_KEYS), such as `description: nil` to clear a description;
33
- # `keep_nil: true` sends every option as given, as for a lens definition.
34
- def call(options, method, keep_nil: false)
35
- operation = [self.class::OPERATIONS, Internal.camelize(method)].compact.join('.')
33
+ # `keep_nil: true` sends every option as given, as for a lens definition. `prefix` names the
34
+ # operation's resource, the resource's own unless the method belongs to the client itself.
35
+ def call(options, method, keep_nil: false, prefix: self.class::OPERATIONS)
36
+ operation = [prefix, Internal.camelize(method)].compact.join('.')
36
37
  check_option_keys(options, operation)
37
38
  api, request = Internal.options(options)
38
39
  nullable = Internal::NULLABLE_OPTION_KEYS.fetch(operation, [])
@@ -84,6 +85,23 @@ module CalmLens
84
85
  end
85
86
  end
86
87
 
88
+ # A list's response, once it is a cursor page: a Hash with an `items` Array. `what` names the
89
+ # items in the `sdk/invalid-response` error otherwise.
90
+ def cursor_page(response, what)
91
+ return response if response.is_a?(Hash) && response['items'].is_a?(Array)
92
+
93
+ raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, "Invalid #{what} page")
94
+ end
95
+
96
+ # Iterates the items of every page `list` returns for `options`, from its `cursor`, yielding each
97
+ # to `block`. `method` names the public iterator, whose options are checked like the list's.
98
+ def each_item(options, method, list, &block)
99
+ api, request = call(options, method) { |*split| split }
100
+ listing = pages(api['cursor']) { |cursor| list.call(api.merge('cursor' => cursor, 'request_options' => request)) }
101
+ listing.each { |page| page['items'].each(&block) }
102
+ nil
103
+ end
104
+
87
105
  # Records a cursor an iteration sends, raising `sdk/invalid-response` (with `details['cursor']`)
88
106
  # when a page with more to follow hands back one it already sent, which would repeat pages forever.
89
107
  def new_cursor!(sent, cursor)
@@ -49,6 +49,14 @@ module CalmLens
49
49
  attr_reader :events
50
50
  # @return [CDN]
51
51
  attr_reader :cdn
52
+ # @return [Lenses]
53
+ attr_reader :lenses
54
+ # @return [UploadPresets]
55
+ attr_reader :upload_presets
56
+ # @return [ProjectSettings]
57
+ attr_reader :project_settings
58
+ # @return [MetadataFields]
59
+ attr_reader :metadata_fields
52
60
 
53
61
  # @param api_key [String] a project API key. Keep it on your server.
54
62
  # @param project_id [String]
@@ -64,7 +72,7 @@ module CalmLens
64
72
  # @param max_response_bytes [Integer] the largest response body read; a larger one fails with
65
73
  # `sdk/response-too-large`, before it is read when its `Content-Length` says so.
66
74
  # @param max_retries [Integer] retries of requests that are safe to repeat: GET/HEAD, and uploads,
67
- # which carry an `Idempotency-Key`. Connection failures, timeouts, 429, 502, 503, 504 and
75
+ # which carry an `Idempotency-Key`. Connection failures, timeouts, 429, 500, 502, 503, 504 and
68
76
  # `idempotency/request-in-progress` are retried with jittered exponential backoff, honouring
69
77
  # `Retry-After`. Other writes are never retried. 0 disables retries.
70
78
  # @param retry_base_delay [Numeric] seconds before the first retry when the API gives no
@@ -98,6 +106,9 @@ module CalmLens
98
106
  @events = Events.new(@session)
99
107
  @cdn = CDN.new(@session)
100
108
  @lenses = Lenses.new(@session)
109
+ @upload_presets = UploadPresets.new(@session)
110
+ @project_settings = ProjectSettings.new(@session)
111
+ @metadata_fields = MetadataFields.new(@session)
101
112
  end
102
113
 
103
114
  # @return [String]
@@ -119,11 +130,11 @@ module CalmLens
119
130
  nil
120
131
  end
121
132
 
122
- # Creates or updates named lenses (saved transforms) and returns them. Definitions
123
- # this client already saved are not sent again. Supplying the same definition and version is
124
- # idempotent; changing a lens needs a higher `version`. More than 100 new definitions are sent in
125
- # consecutive requests, in order.
126
- # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, version:, transform:, ...}]`
133
+ # Creates named lenses (saved transforms), or changes them to these definitions, and returns
134
+ # them. Definitions this client already saved are not sent again, and sending the same
135
+ # definition again returns the lens unchanged. More than 100 new definitions are sent in
136
+ # consecutive requests, in order. {#lenses} manages lenses one by one.
137
+ # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, active:, transform:, ...}]`
127
138
  # @return [Array<Hash>] the saved lenses, in the given order.
128
139
  def ensure_lenses(options) = @lenses.ensure_lenses(options)
129
140
 
@@ -10,7 +10,7 @@ module CalmLens
10
10
  # The resources share one session per client. It never prints the API key.
11
11
  # @api private
12
12
  class Session
13
- RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
13
+ RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
14
14
  USER_AGENT = "calmlens-ruby/#{VERSION} (ruby/#{RUBY_VERSION})".freeze
15
15
 
16
16
  # Raised through the transport so an exception from the caller's streaming block is
@@ -99,10 +99,11 @@ module CalmLens
99
99
 
100
100
  # Sends a CDN request, retrying like an API read, and returns the last response rather than
101
101
  # raising an error status. With a block, yields the streaming {Response} and returns the block's
102
- # value; otherwise returns the response with its body read.
103
- def cdn(method, url, headers, request_options, &block)
102
+ # value; otherwise returns the response with its body read. `authenticate: false` sends no API
103
+ # key, as a browser would.
104
+ def cdn(method, url, headers, request_options, authenticate: true, &block)
104
105
  limit = request_options.fetch(:max_response_bytes, nil)
105
- execute(method, url, headers, nil, request_options, allow_http_errors: true) do |response|
106
+ execute(method, url, headers, nil, request_options, allow_http_errors: true, authenticate: authenticate) do |response|
106
107
  next response.tap { response.body(limit: limit) } unless block
107
108
 
108
109
  streaming = Response.new(status: response.status, headers: response.headers) do |&sink|
@@ -122,6 +123,14 @@ module CalmLens
122
123
  end
123
124
  end
124
125
 
126
+ # Fetches JSON from the CDN without the API key, as a browser would, and returns it parsed. An
127
+ # error status raises the error the CDN's body names, such as `signed-url/expired`.
128
+ def cdn_json(url)
129
+ execute('GET', url, { 'Accept' => 'application/json' }, nil, {}, authenticate: false) do |response|
130
+ parse('GET', response, @max_response_bytes)
131
+ end
132
+ end
133
+
125
134
  # Resolves lens definitions, sending only definitions this client has not saved before, at most
126
135
  # ServerClient::ENSURE_LENSES_MAX_PER_REQUEST per request, in order. Calls are serialized, so concurrent calls
127
136
  # for the same definitions send them once.
@@ -142,7 +151,7 @@ module CalmLens
142
151
  raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Unexpected lens response')
143
152
  end
144
153
 
145
- # A new version of a lens replaces the cached definition with the same identifier.
154
+ # A changed lens replaces the cached definition with the same identifier.
146
155
  chunk.keys.zip(returned).each do |key, lens|
147
156
  @lenses.delete_if { |_, saved| saved['identifier'] == lens['identifier'] }
148
157
  @lenses[key] = lens
@@ -156,9 +165,16 @@ module CalmLens
156
165
  end
157
166
  end
158
167
 
159
- def headers_for(extra)
160
- extra.reject { |name, _| %w[x-api-key user-agent authorization].include?(name.downcase) }
161
- .merge('X-Api-Key' => @api_key, 'User-Agent' => USER_AGENT)
168
+ # Drops the cached definitions of the lenses the block selects, which lenses.create, update
169
+ # and delete changed, so ensure_lenses sends their definitions again.
170
+ def forget_lenses(&selected)
171
+ @lens_lock.synchronize { @lenses.delete_if { |_, lens| selected.call(lens) } }
172
+ end
173
+
174
+ def headers_for(extra, authenticate: true)
175
+ headers = extra.reject { |name, _| %w[x-api-key user-agent authorization].include?(name.downcase) }
176
+ .merge('User-Agent' => USER_AGENT)
177
+ authenticate ? headers.merge('X-Api-Key' => @api_key) : headers
162
178
  end
163
179
 
164
180
  private
@@ -168,7 +184,7 @@ module CalmLens
168
184
  def lens_key(lens)
169
185
  fields = { 'identifier' => lens['identifier'], 'name' => lens['name'], 'description' => lens['description'],
170
186
  'type' => lens['type'], 'transform' => lens['transform'] }
171
- %w[active version].each { |name| fields[name] = lens[name] if lens.key?(name) }
187
+ fields['active'] = lens['active'] if lens.key?('active')
172
188
  JSON.generate(canonical(fields))
173
189
  end
174
190
 
@@ -186,14 +202,15 @@ module CalmLens
186
202
  returned.zip(sent).all? { |saved, definition| saved.is_a?(Hash) && saved['identifier'] == definition['identifier'] }
187
203
  end
188
204
 
189
- # Sends a request, retrying transport failures, timeouts, 429, 502–504 and
205
+ # Sends a request, retrying transport failures, timeouts, 429, 500, 502–504 and
190
206
  # `idempotency/request-in-progress` when it is safe to repeat: reads, and requests with an
191
207
  # `Idempotency-Key` whose body can be sent again. Redirects are never followed.
192
- def execute(method, url, headers, body, request_options, allow_http_errors: false, deadline: nil, &handler)
208
+ def execute(method, url, headers, body, request_options, allow_http_errors: false, deadline: nil, authenticate: true, &handler)
193
209
  retries = request_options.fetch(:max_retries, @max_retries)
194
210
  retryable = retries.positive? && (%w[GET HEAD].include?(method) || headers.key?('Idempotency-Key')) &&
195
211
  (body.nil? || body.replayable?)
196
- request = Transport::Request.new(method: method, url: url, headers: headers_for(headers), body: body&.content,
212
+ request = Transport::Request.new(method: method, url: url, headers: headers_for(headers, authenticate: authenticate),
213
+ body: body&.content,
197
214
  open_timeout: request_options.fetch(:open_timeout, @open_timeout),
198
215
  timeout: request_options.fetch(:timeout, @timeout))
199
216
  limit = request_options.fetch(:max_response_bytes, @max_response_bytes)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by packages/sdk-contract/mockAssetFilters.ts from ASSET_FILTER_FIELDS (packages/shared-backend/src/AssetFilters.ts) and listFilterOperators (PaginationUtils.ts); do not edit.
4
+ # A filter may name only these fields, each with only its operators; anything else is
5
+ # filter/invalid. A number field's value must be a finite number, and `null` is a
6
+ # missing value only on a nullable field.
7
+ module CalmLens
8
+ module MockAssetFilters
9
+ # Each field a filter may name, with its kind, whether it is nullable and its operators.
10
+ FIELDS = {
11
+ 'id' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
12
+ 'parentId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
13
+ 'folderId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
14
+ 'uploadPresetId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
15
+ 'createdAt' => { kind: 'number', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
16
+ 'updatedAt' => { kind: 'number', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
17
+ 'type' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
18
+ 'name' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
19
+ 'description' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
20
+ 'externalId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
21
+ 'externalUrl' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
22
+ 'authorId' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
23
+ 'tags' => { kind: 'list', nullable: false, operators: ['=in=', '=out='].freeze }.freeze,
24
+ 'visibility' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
25
+ 'upload.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
26
+ 'upload.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
27
+ 'classification.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
28
+ 'classification.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
29
+ 'classification.completedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
30
+ 'classification.moderation.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
31
+ 'content.status' => { kind: 'text', nullable: false, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
32
+ 'content.removalReason' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
33
+ 'content.updatedAt' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
34
+ 'content.format' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
35
+ 'content.extension' => { kind: 'text', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
36
+ 'content.sizeInBytes' => { kind: 'number', nullable: true, operators: ['==', '!=', '>', '>=', '<', '<=', '=in=', '=out='].freeze }.freeze,
37
+ }.freeze
38
+ end
39
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Generated by packages/sdk-contract/mockClassification.ts from packages/shared/src/ModerationCategories.ts and ModerationPolicyDimensions.ts; do not edit.
3
+ # Generated by packages/sdk-contract/mockClassification.ts from packages/shared/src/ModerationCategories.ts, ModerationPolicyDimensions.ts and TestTriggers.ts; do not edit.
4
4
  module CalmLens
5
5
  module MockClassification
6
6
  # The parent categories, in the API's order.
@@ -8,7 +8,7 @@ module CalmLens
8
8
 
9
9
  # Each category's qualifiers.
10
10
  QUALIFIERS = {
11
- "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
11
+ "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-strangulation", "sexual-incest", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
12
12
  "violence" => ["violence-high-impact", "violence-attack", "violence-threat", "violence-preparation", "violence-encouragement", "violence-wish", "violence-coded-incitement", "violence-extremism", "violence-injury", "violence-violent-death", "violence-natural-death", "violence-graphic", "violence-decapitation-act", "violence-gore", "violence-weapon", "violence-sport", "violence-text", "violence-historical", "violence-documentary", "violence-educational", "violence-discussion", "violence-fictional", "violence-fantasy", "violence-support-prevention"].freeze,
13
13
  "self-harm" => ["self-harm-age-restricted", "self-harm-thoughts", "self-harm-preparation", "self-harm-act", "self-harm-methods", "self-harm-encouragement", "self-harm-historical", "self-harm-documentary", "self-harm-educational", "self-harm-fictional", "self-harm-support-prevention"].freeze,
14
14
  "harassment" => ["harassment-abuse", "harassment-mild", "harassment-bullying", "harassment-public-target", "harassment-threat", "harassment-sexual", "harassment-encouragement", "harassment-historical", "harassment-documentary", "harassment-educational", "harassment-fictional", "harassment-support-prevention"].freeze,
@@ -20,5 +20,11 @@ module CalmLens
20
20
 
21
21
  # Qualifiers the input type sets, which a test input scenario cannot list.
22
22
  INPUT_QUALIFIERS = ["sexual-text", "violence-text"].freeze
23
+
24
+ # Test scenario detections: entries per list, the basis points of a full side, the oldest age and the age presets.
25
+ TEST_DETECTIONS_MAX = 32
26
+ TEST_DETECTION_BOX_MAX = 10000
27
+ TEST_FACE_AGE_MAX = 120
28
+ TEST_FACE_AGE_PRESETS = ["adult", "minor"].freeze
23
29
  end
24
30
  end