lato_cms 3.2.6 → 3.3.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 +4 -4
- data/README.md +33 -0
- data/app/controllers/lato_cms/media_controller.rb +9 -8
- data/app/jobs/lato_cms/generate_media_text_job.rb +6 -5
- data/app/models/lato_cms/media.rb +73 -37
- data/app/views/lato_cms/media/_form_update.html.erb +18 -0
- data/app/views/lato_cms/media/_translations_field.html.erb +3 -17
- data/config/locales/en.yml +1 -1
- data/config/locales/it.yml +1 -1
- data/config/routes.rb +1 -1
- data/lib/lato_cms/config.rb +22 -2
- data/lib/lato_cms/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a5b87d2c477cafb579b7ceea01c41e72071b3a789f425e184acbe99592f63ab
|
|
4
|
+
data.tar.gz: 123893a3b2a7c226c4910f41ab931b5bf7fd28b30b883293d811e6fc439d0155
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23968a738f1d1295e172d546617950e2b49c900b45ac6399080a01380c97ae7ebba48414277c58565205f8229dd758b798a286a3dfb6eac55d3c1091258fad12
|
|
7
|
+
data.tar.gz: 0a3d712b435da91e70a4a90edd8cefad81fa8105dface4946c4776490b920d3f2259c4857619d99af374e84b181aea38bc0524b2df1ef6f22532f80a141d16c5
|
data/README.md
CHANGED
|
@@ -43,6 +43,39 @@ import "lato_cms/application";
|
|
|
43
43
|
// ....
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Media URLs
|
|
47
|
+
|
|
48
|
+
By default a media URL is an Active Storage **redirect** URL: Rails answers 302 with a
|
|
49
|
+
signed address that expires. On a remote service (S3 and friends) that is what you want —
|
|
50
|
+
the file then comes from the service itself.
|
|
51
|
+
|
|
52
|
+
On a **disk** service it is the wrong trade: the redirect costs a second request, and its
|
|
53
|
+
target is served `private, must-revalidate`, so no browser, proxy or CDN can keep it. A
|
|
54
|
+
video of a few dozen MB is then downloaded again on every visit, from a thread of your app
|
|
55
|
+
server. Switch the mode:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
LatoCms.configure do |config|
|
|
59
|
+
config.media_url_mode = :proxy
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Media URLs (the blob and every variant) then point at Active Storage's proxy controller:
|
|
64
|
+
no redirect, and `Cache-Control: public, immutable`, which a cache in front of the app can
|
|
65
|
+
actually use. The address carries the signature of the blob, so a replaced file is a
|
|
66
|
+
different address and can never be served stale.
|
|
67
|
+
|
|
68
|
+
⚠️ **`:proxy` needs something in front that splits byte ranges** — nginx with `slice`,
|
|
69
|
+
or a CDN that does the same. Active Storage's proxy controller serves one range at a
|
|
70
|
+
time, holding it in memory, and answers **416** to anything above
|
|
71
|
+
`streaming_chunk_max_size` (100 MB by default). A `<video>` opens with `Range: bytes=0-`,
|
|
72
|
+
that is the whole file: above that size the browser reads the 416 as "format not
|
|
73
|
+
supported", and below it the app loads tens of MB into memory per request. With slices of
|
|
74
|
+
1 MB neither happens. Without such a front, stay on `:redirect`.
|
|
75
|
+
|
|
76
|
+
Both modes ask for an `inline` disposition: a media of the library is content to show, and
|
|
77
|
+
the default for a video would otherwise turn opening its address into a download.
|
|
78
|
+
|
|
46
79
|
## Development
|
|
47
80
|
|
|
48
81
|
Clone repository, install dependencies, run migrations and start:
|
|
@@ -81,16 +81,17 @@ module LatoCms
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Regenerates
|
|
85
|
-
# route constraint) with the LLM. Runs as a Lato::Operation
|
|
86
|
-
# GenerateMediaTextJob) rather than inline: an LLM call can take a
|
|
87
|
-
# and blocking the request risks timing it out. The admin instead
|
|
88
|
-
# a live progress page.
|
|
84
|
+
# Regenerates the translatable attributes (alt_text and title, or a single
|
|
85
|
+
# one — see the route constraint) with the LLM. Runs as a Lato::Operation
|
|
86
|
+
# (see GenerateMediaTextJob) rather than inline: an LLM call can take a
|
|
87
|
+
# while, and blocking the request risks timing it out. The admin instead
|
|
88
|
+
# lands on a live progress page.
|
|
89
89
|
def regenerate_text_action
|
|
90
90
|
@media = query_media.find(params[:id])
|
|
91
|
-
|
|
91
|
+
attributes = params[:attribute] == "all" ? LatoCms.config.llm_media_attributes : [params[:attribute]]
|
|
92
|
+
attributes = attributes.map(&:to_s).select { |attribute| LatoCms.config.llm_generates?(attribute) }
|
|
92
93
|
|
|
93
|
-
unless @media.image? &&
|
|
94
|
+
unless @media.image? && attributes.any?
|
|
94
95
|
respond_to do |format|
|
|
95
96
|
message = t("lato_cms.media_text_regenerate_unavailable")
|
|
96
97
|
format.html { redirect_to lato_cms.media_update_path(@media), alert: message }
|
|
@@ -99,7 +100,7 @@ module LatoCms
|
|
|
99
100
|
return
|
|
100
101
|
end
|
|
101
102
|
|
|
102
|
-
operation = Lato::Operation.generate("LatoCms::GenerateMediaTextJob", { media_id: @media.id, attributes:
|
|
103
|
+
operation = Lato::Operation.generate("LatoCms::GenerateMediaTextJob", { media_id: @media.id, attributes: attributes }, @session.user_id)
|
|
103
104
|
|
|
104
105
|
if operation.start
|
|
105
106
|
redirect_to lato.operation_path(operation)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module LatoCms
|
|
2
2
|
# Generates alt text and/or title for an image Media via an OpenAI-compatible
|
|
3
|
-
# LLM
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
3
|
+
# LLM in a single call, whatever the number of attributes: the image travels
|
|
4
|
+
# in the request, so one call per attribute meant uploading it twice (see
|
|
5
|
+
# Media#generate_texts!). `attributes` defaults to every attribute enabled in
|
|
6
|
+
# config, which is what both the automatic post-upload run and the
|
|
7
|
+
# "Regenerate with AI" action ask for.
|
|
7
8
|
#
|
|
8
9
|
# Doubles as a plain background job (post-upload call site, see
|
|
9
10
|
# Media#enqueue_text_generation: fire-and-forget, best effort, never blocks
|
|
@@ -19,7 +20,7 @@ module LatoCms
|
|
|
19
20
|
return unless media
|
|
20
21
|
|
|
21
22
|
attributes = Array(params[:attributes]).presence || LatoCms.config.llm_media_attributes
|
|
22
|
-
|
|
23
|
+
media.generate_texts!(attributes, raise_on_error: operation?)
|
|
23
24
|
save_operation_output_message(I18n.t("lato_cms.media_text_regenerated")) if operation?
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -154,11 +154,27 @@ module LatoCms
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
def url
|
|
157
|
-
|
|
157
|
+
attachment_url(file) if file.attached?
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
def poster_url
|
|
161
|
-
|
|
161
|
+
attachment_url(poster_file) if poster_file.attached?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The URL of an attachment or of one of its variants, in the mode the host
|
|
165
|
+
# app asked for (see LatoCms::Config::MEDIA_URL_MODES).
|
|
166
|
+
#
|
|
167
|
+
# `inline` in both modes: a media of the library is content to show — a
|
|
168
|
+
# picture in a page, a video in a player — and the default disposition of a
|
|
169
|
+
# video is `attachment`, which turns opening its address into a download.
|
|
170
|
+
# Whoever wants a download asks for it explicitly, as the panel does.
|
|
171
|
+
def attachment_url(attachable)
|
|
172
|
+
helpers = Rails.application.routes.url_helpers
|
|
173
|
+
if LatoCms.config.media_url_proxy?
|
|
174
|
+
helpers.rails_storage_proxy_path(attachable, only_path: true, disposition: :inline)
|
|
175
|
+
else
|
|
176
|
+
helpers.rails_blob_path(attachable, only_path: true, disposition: :inline)
|
|
177
|
+
end
|
|
162
178
|
end
|
|
163
179
|
|
|
164
180
|
# Swaps the underlying file while keeping the same record, so every field
|
|
@@ -196,50 +212,68 @@ module LatoCms
|
|
|
196
212
|
Rails.logger.warn("LatoCms: failed to generate video poster for media #{id}: #{e.message}")
|
|
197
213
|
end
|
|
198
214
|
|
|
199
|
-
# Asks the configured OpenAI-compatible LLM for
|
|
200
|
-
# title) in every configured locale and merges
|
|
201
|
-
# translations for locales the LLM didn't return are
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
215
|
+
# Asks the configured OpenAI-compatible LLM for the given attributes
|
|
216
|
+
# (alt_text, title, or both) in every configured locale and merges the
|
|
217
|
+
# result in — existing translations for locales the LLM didn't return are
|
|
218
|
+
# kept, the rest are replaced individually. Attributes switched off in
|
|
219
|
+
# config are skipped; no-op unless the LLM is configured and this media is
|
|
220
|
+
# an image, since the model has to look at the file.
|
|
221
|
+
#
|
|
222
|
+
# All the requested attributes travel in ONE request: the image is the
|
|
223
|
+
# expensive part of the payload (it goes as a base64 data URI, see
|
|
224
|
+
# request_completion), so asking per attribute uploaded the same file twice
|
|
225
|
+
# and paid for it twice.
|
|
205
226
|
#
|
|
206
227
|
# Best effort by default (`raise_on_error: false`): any failure is logged
|
|
207
228
|
# and swallowed, since the automatic post-upload call site (see
|
|
208
229
|
# GenerateMediaTextJob) must never break the upload over a flaky LLM. The
|
|
209
|
-
# manual "Regenerate with AI"
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
# manual "Regenerate with AI" action runs as a Lato::Operation the admin is
|
|
231
|
+
# actively watching, so it passes `raise_on_error: true` to have failures
|
|
232
|
+
# surface there instead of disappearing silently.
|
|
233
|
+
def generate_texts!(attributes = LatoCms.config.llm_media_attributes, raise_on_error: false)
|
|
234
|
+
attributes = Array(attributes).map(&:to_s)
|
|
235
|
+
unknown = attributes - TRANSLATABLE_ATTRIBUTES
|
|
236
|
+
raise ArgumentError, "unknown translatable attribute: #{unknown.join(", ")}" if unknown.any?
|
|
237
|
+
|
|
238
|
+
attributes = attributes.select { |attribute| LatoCms.config.llm_generates?(attribute) }
|
|
239
|
+
return if attributes.empty? || !image? || !file.attached?
|
|
216
240
|
|
|
217
241
|
begin
|
|
218
|
-
|
|
219
|
-
raise "The LLM returned no usable #{
|
|
242
|
+
generated = parse_translations(request_completion(llm_prompt(*attributes)), attributes)
|
|
243
|
+
raise "The LLM returned no usable #{attributes.to_sentence}" if generated.blank?
|
|
220
244
|
|
|
221
|
-
public_send("#{attribute}_translations=", translations_of(attribute).merge(translations))
|
|
245
|
+
generated.each { |attribute, translations| public_send("#{attribute}_translations=", translations_of(attribute).merge(translations)) }
|
|
222
246
|
save!
|
|
223
247
|
rescue StandardError => e
|
|
224
|
-
Rails.logger.warn("LatoCms: failed to generate #{
|
|
248
|
+
Rails.logger.warn("LatoCms: failed to generate #{attributes.join(", ")} for media #{id}: #{e.message}")
|
|
225
249
|
raise if raise_on_error
|
|
226
250
|
end
|
|
227
251
|
end
|
|
228
252
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
253
|
+
def generate_text!(attribute, raise_on_error: false)
|
|
254
|
+
generate_texts!([attribute], raise_on_error: raise_on_error)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Full prompt sent for `attributes`: one custom-or-default task prompt per
|
|
258
|
+
# attribute (see LatoCms::Config#llm_prompt) with {languages}/{urls}
|
|
259
|
+
# substituted, plus the engine's fixed response-format contract, appended
|
|
260
|
+
# here rather than left to the prompts so parsing never depends on how a
|
|
261
|
+
# custom one is worded.
|
|
262
|
+
def llm_prompt(*attributes)
|
|
263
|
+
attributes = attributes.flatten.map(&:to_s).presence || LatoCms.config.llm_media_attributes.map(&:to_s)
|
|
234
264
|
variables = {
|
|
235
265
|
"languages" => llm_locales.join(", "),
|
|
236
266
|
"urls" => usage_urls.presence&.join(", ") || "none"
|
|
237
267
|
}
|
|
238
268
|
placeholders = /\{(#{LatoCms::Config::LLM_PROMPT_VARIABLES.join("|")})\}/
|
|
239
|
-
|
|
269
|
+
tasks = attributes.map do |attribute|
|
|
270
|
+
task = LatoCms.config.llm_prompt(attribute).gsub(placeholders) { variables[Regexp.last_match(1)] }
|
|
271
|
+
"#{attribute}: #{task}"
|
|
272
|
+
end
|
|
240
273
|
|
|
241
|
-
"#{
|
|
242
|
-
"keys: #{
|
|
274
|
+
"#{tasks.join("\n\n")}\n\nRespond with a single JSON object only, no markdown, no extra text, with exactly " \
|
|
275
|
+
"these keys: #{attributes.join(", ")}. Each value is an object with exactly these keys: " \
|
|
276
|
+
"#{llm_locales.join(", ")}, each holding the text written in that language."
|
|
243
277
|
end
|
|
244
278
|
|
|
245
279
|
# Fixed small variant used across admin UI (Media index, picker grid, field
|
|
@@ -248,9 +282,7 @@ module LatoCms
|
|
|
248
282
|
def thumbnail_url
|
|
249
283
|
return nil unless image? && file.attached? && file.variable?
|
|
250
284
|
|
|
251
|
-
|
|
252
|
-
file.variant(resize_to_fill: [200, 200]), only_path: true
|
|
253
|
-
)
|
|
285
|
+
attachment_url(file.variant(resize_to_fill: [200, 200]))
|
|
254
286
|
rescue StandardError => e
|
|
255
287
|
Rails.logger.error("LatoCms: Failed to build thumbnail for media #{id}: #{e.message}")
|
|
256
288
|
nil
|
|
@@ -263,9 +295,7 @@ module LatoCms
|
|
|
263
295
|
return nil unless image? && file.attached?
|
|
264
296
|
return url unless file.variable?
|
|
265
297
|
|
|
266
|
-
|
|
267
|
-
file.variant(resize_to_limit: [1200, 1200]), only_path: true
|
|
268
|
-
)
|
|
298
|
+
attachment_url(file.variant(resize_to_limit: [1200, 1200]))
|
|
269
299
|
rescue StandardError => e
|
|
270
300
|
Rails.logger.error("LatoCms: Failed to build preview for media #{id}: #{e.message}")
|
|
271
301
|
url
|
|
@@ -278,12 +308,11 @@ module LatoCms
|
|
|
278
308
|
def variant_urls(sizes_config)
|
|
279
309
|
return {} if sizes_config.blank? || !sizes_config.respond_to?(:each_pair) || !file.attached? || !file.variable?
|
|
280
310
|
|
|
281
|
-
url_helpers = Rails.application.routes.url_helpers
|
|
282
311
|
sizes_config.each_with_object({}) do |(name, opts), acc|
|
|
283
312
|
transformation = self.class.variant_transformation(opts)
|
|
284
313
|
next if transformation.blank?
|
|
285
314
|
|
|
286
|
-
acc[name] =
|
|
315
|
+
acc[name] = attachment_url(file.variant(transformation))
|
|
287
316
|
end
|
|
288
317
|
rescue StandardError => e
|
|
289
318
|
Rails.logger.error("LatoCms: Failed to build image variants for media #{id}: #{e.message}")
|
|
@@ -354,11 +383,18 @@ module LatoCms
|
|
|
354
383
|
}])
|
|
355
384
|
end
|
|
356
385
|
|
|
357
|
-
|
|
386
|
+
# { attribute => { locale => text } }, keeping only what was asked for and
|
|
387
|
+
# what the app is configured to speak.
|
|
388
|
+
def parse_translations(content, attributes)
|
|
358
389
|
return {} if content.blank?
|
|
359
390
|
|
|
360
391
|
parsed = JSON.parse(content[/\{.*\}/m] || content)
|
|
361
|
-
parsed.slice(*
|
|
392
|
+
parsed.slice(*attributes).each_with_object({}) do |(attribute, translations), result|
|
|
393
|
+
next unless translations.is_a?(Hash)
|
|
394
|
+
|
|
395
|
+
translations = translations.slice(*llm_locales).transform_values(&:to_s).compact_blank
|
|
396
|
+
result[attribute] = translations if translations.any?
|
|
397
|
+
end
|
|
362
398
|
rescue JSON::ParserError
|
|
363
399
|
{}
|
|
364
400
|
end
|
|
@@ -33,6 +33,24 @@
|
|
|
33
33
|
<%= lato_form_item_input_text form, :name, required: true %>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
+
<%# One action for every attribute the LLM is enabled for: the image
|
|
37
|
+
travels in the request, so asking per attribute uploaded the same
|
|
38
|
+
file once per attribute. %>
|
|
39
|
+
<% llm_attributes = media.image? ? LatoCms.config.llm_media_attributes : [] %>
|
|
40
|
+
<% if llm_attributes.any? %>
|
|
41
|
+
<div class="d-flex justify-content-end mb-2">
|
|
42
|
+
<%= link_to t("lato_cms.media_text_regenerate"), lato_cms.media_regenerate_text_action_path(media, attribute: "all"),
|
|
43
|
+
class: "btn btn-sm btn-outline-secondary",
|
|
44
|
+
data: {
|
|
45
|
+
lato_action_target: "trigger",
|
|
46
|
+
turbo_method: :post,
|
|
47
|
+
turbo_frame: "lato_operation",
|
|
48
|
+
turbo_confirm: t("lato_cms.media_text_regenerate_confirm", fields: llm_attributes.map { |attribute| t("lato_cms.media_#{attribute}").downcase }.join(", ")),
|
|
49
|
+
action_title: t("lato_cms.media_text_regenerate")
|
|
50
|
+
} %>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
36
54
|
<% if media.image? %>
|
|
37
55
|
<%= render "lato_cms/media/translations_field", form: form, media: media, attribute: "alt_text" %>
|
|
38
56
|
<% end %>
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
<%# One tab per configured locale for a translatable Media attribute
|
|
2
|
-
(alt_text/title)
|
|
3
|
-
|
|
4
|
-
to look at the file to describe it. %>
|
|
2
|
+
(alt_text/title). The "Regenerate with AI" action is not here: it covers
|
|
3
|
+
every enabled attribute in one LLM call (see _form_update). %>
|
|
5
4
|
<% label = t("lato_cms.media_#{attribute}") %>
|
|
6
5
|
|
|
7
6
|
<div class="mb-3">
|
|
8
|
-
<
|
|
9
|
-
<label class="form-label mb-0"><%= label %></label>
|
|
10
|
-
<% if media.image? && LatoCms.config.llm_generates?(attribute) %>
|
|
11
|
-
<%= link_to t("lato_cms.media_text_regenerate"), lato_cms.media_regenerate_text_action_path(media, attribute: attribute),
|
|
12
|
-
class: "btn btn-sm btn-outline-secondary",
|
|
13
|
-
data: {
|
|
14
|
-
lato_action_target: "trigger",
|
|
15
|
-
turbo_method: :post,
|
|
16
|
-
turbo_frame: "lato_operation",
|
|
17
|
-
turbo_confirm: t("lato_cms.media_text_regenerate_confirm", field: label.downcase),
|
|
18
|
-
action_title: "#{t('lato_cms.media_text_regenerate')} - #{label}"
|
|
19
|
-
} %>
|
|
20
|
-
<% end %>
|
|
21
|
-
</div>
|
|
7
|
+
<label class="form-label"><%= label %></label>
|
|
22
8
|
|
|
23
9
|
<ul class="nav nav-tabs">
|
|
24
10
|
<% LatoCms.config.locales.each_with_index do |locale, index| %>
|
data/config/locales/en.yml
CHANGED
|
@@ -105,7 +105,7 @@ en:
|
|
|
105
105
|
media_alt_text: Alt text
|
|
106
106
|
media_title: Title
|
|
107
107
|
media_text_regenerate: Regenerate with AI
|
|
108
|
-
media_text_regenerate_confirm: "This will overwrite the current %{
|
|
108
|
+
media_text_regenerate_confirm: "This will overwrite the current values of %{fields} in every language with AI-generated ones. Continue?"
|
|
109
109
|
media_text_regenerated: Text regenerated
|
|
110
110
|
media_text_regenerate_unavailable: AI generation is only available for images, and only when an LLM is configured and enabled for this field
|
|
111
111
|
media_text_regenerate_failed: Failed to start AI generation
|
data/config/locales/it.yml
CHANGED
|
@@ -105,7 +105,7 @@ it:
|
|
|
105
105
|
media_alt_text: Testo alternativo
|
|
106
106
|
media_title: Titolo
|
|
107
107
|
media_text_regenerate: Rigenera con AI
|
|
108
|
-
media_text_regenerate_confirm: "
|
|
108
|
+
media_text_regenerate_confirm: "I valori attuali di %{fields} in ogni lingua verranno sostituiti con testi generati dall'AI. Continuare?"
|
|
109
109
|
media_text_regenerated: Testo rigenerato
|
|
110
110
|
media_text_regenerate_unavailable: La generazione AI è disponibile solo per le immagini e solo se un LLM è configurato e abilitato per questo campo
|
|
111
111
|
media_text_regenerate_failed: Impossibile avviare la generazione AI
|
data/config/routes.rb
CHANGED
|
@@ -34,7 +34,7 @@ LatoCms::Engine.routes.draw do
|
|
|
34
34
|
get ':id/update', to: 'media#update', as: :media_update
|
|
35
35
|
patch ':id/update', to: 'media#update_action', as: :media_update_action
|
|
36
36
|
patch ':id/replace-file', to: 'media#replace_file_action', as: :media_replace_file_action
|
|
37
|
-
post ':id/regenerate/:attribute', to: 'media#regenerate_text_action', as: :media_regenerate_text_action, constraints: { attribute: /alt_text|title/ }
|
|
37
|
+
post ':id/regenerate/:attribute', to: 'media#regenerate_text_action', as: :media_regenerate_text_action, constraints: { attribute: /alt_text|title|all/ }
|
|
38
38
|
delete ':id', to: 'media#destroy_action', as: :media_destroy_action
|
|
39
39
|
end
|
|
40
40
|
end
|
data/lib/lato_cms/config.rb
CHANGED
|
@@ -3,8 +3,19 @@ module LatoCms
|
|
|
3
3
|
# This class contains the default configuration of the engine.
|
|
4
4
|
##
|
|
5
5
|
class Config
|
|
6
|
-
attr_accessor :locales, :templates_path, :admin_roles, :
|
|
7
|
-
:llm_generate_alt_text, :llm_generate_title, :llm_alt_text_prompt,
|
|
6
|
+
attr_accessor :locales, :templates_path, :admin_roles, :media_url_mode, :llm_api_url, :llm_model,
|
|
7
|
+
:llm_api_key, :llm_generate_alt_text, :llm_generate_title, :llm_alt_text_prompt,
|
|
8
|
+
:llm_title_prompt
|
|
9
|
+
|
|
10
|
+
# How the URLs of media files are built (see LatoCms::Media#url).
|
|
11
|
+
# - :redirect — Rails answers 302 with a signed URL that expires; on a
|
|
12
|
+
# remote service (S3 and friends) the file then comes from the service
|
|
13
|
+
# itself, which is what you want there.
|
|
14
|
+
# - :proxy — the file is served through the app, with no redirect and with
|
|
15
|
+
# `Cache-Control: public, immutable`, so browsers and any cache in front
|
|
16
|
+
# can keep it. On a disk service this is strictly better: the redirect
|
|
17
|
+
# costs a second request and its target can be cached by nobody.
|
|
18
|
+
MEDIA_URL_MODES = %i[redirect proxy].freeze
|
|
8
19
|
|
|
9
20
|
# Placeholders substituted at request time in both default and custom LLM
|
|
10
21
|
# prompts (see LatoCms::Media#llm_prompt):
|
|
@@ -37,6 +48,11 @@ module LatoCms
|
|
|
37
48
|
# (create, update, delete) and translation links.
|
|
38
49
|
@admin_roles = { none: 0, operator: 1, admin: 2 }
|
|
39
50
|
|
|
51
|
+
# Default is :redirect, which is how it always worked: switching the way
|
|
52
|
+
# every media URL is built is a decision for the host app, not something
|
|
53
|
+
# an upgrade should do on its own. See MEDIA_URL_MODES.
|
|
54
|
+
@media_url_mode = :redirect
|
|
55
|
+
|
|
40
56
|
# Optional OpenAI-compatible endpoint used to auto-generate alt text and
|
|
41
57
|
# title for uploaded images (see LatoCms::Media#generate_text!). All
|
|
42
58
|
# three must be set for the feature to activate; unset by default.
|
|
@@ -56,6 +72,10 @@ module LatoCms
|
|
|
56
72
|
@llm_title_prompt = nil
|
|
57
73
|
end
|
|
58
74
|
|
|
75
|
+
def media_url_proxy?
|
|
76
|
+
media_url_mode.to_sym == :proxy
|
|
77
|
+
end
|
|
78
|
+
|
|
59
79
|
def llm_configured?
|
|
60
80
|
llm_api_url.present? && llm_model.present? && llm_api_key.present?
|
|
61
81
|
end
|
data/lib/lato_cms/version.rb
CHANGED