An Active Storage transformer that sends image and video variant processing to the Crucible web service. Built on top of active_storage-async_variants.
Processing image variants and video previews on your Rails server ties up workers and requires installing tools like vips and ffmpeg in production. Crucible is an external service that handles these transformations -- but you need a bridge between Active Storage's variant system and Crucible's HTTP API.
This gem provides that bridge. It implements the async_variants external transformer interface, delegating all image/video processing to Crucible via presigned S3 URLs.
gem "active_storage-crucible"Requires an S3-compatible storage service (the gem generates presigned URLs for Crucible to read source files and write results).
# config/initializers/crucible.rb
ActiveStorage::Crucible.endpoint = "https://crucible.example.com"Or with a block:
ActiveStorage::Crucible.configure do |config|
config.endpoint = ENV["CRUCIBLE_ENDPOINT"]
endUse ActiveStorage::Crucible::Transformer as the transformer for any variant:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb,
resize_to_limit: [100, 100],
format: :webp,
transformer: ActiveStorage::Crucible::Transformer,
fallback: :original
end
has_one_attached :video do |attachable|
attachable.variant :web,
resize_to_limit: [1280, 720],
format: :webp,
transformer: ActiveStorage::Crucible::Transformer,
fallback: :original
end
endThe transformer auto-detects image vs. video based on the blob's content type and calls the appropriate Crucible endpoint (/image/variant or /video/variant).
In views, use standard Active Storage helpers:
<%= image_tag user.avatar.variant(:thumb).url %>While the variant is processing, this serves the original file. Once Crucible finishes and calls back, it serves the processed variant.
The gem also extends ActiveStorage::Preview to process video previews through Crucible. This happens automatically for video blobs on S3-compatible services -- no extra configuration needed.
<%= image_tag user.video.preview(resize_to_limit: [640, 480], format: :webp).url %>While the preview is processing, the original video URL is served as a fallback.
- A file is attached to a model with a Crucible-backed variant defined
async_variantsenqueues a background job for the variant- The job calls
Crucible::Transformer#initiate, which:- Creates a placeholder output blob in the database
- Attaches it to the variant record
- Generates presigned GET/PUT URLs for the source and output blobs
- POSTs to Crucible with the URLs, dimensions, format, and a signed callback URL
- Crucible processes the image/video, uploads the result to the presigned PUT URL
- Crucible POSTs to the callback URL with
{"status": "success"} - The
async_variantscallback controller marks the variant record as processed
- A video preview is requested in a view
PreviewExtension#processcreates placeholder blobs for the preview image and its variant- POSTs to Crucible's
/video/previewendpoint with presigned URLs and a callback URL - Crucible extracts a frame, resizes it, uploads both the preview image and variant
- Crucible POSTs to the callback URL to mark the variant as processed
Variant requests (POST /image/variant or /video/variant):
{
"blob_url": "https://s3.example.com/source?presigned...",
"variant_url": "https://s3.example.com/output?presigned...",
"dimensions": "100x100",
"rotation": 0,
"format": "webp",
"callback_url": "https://app.example.com/active_storage/async_variants/callbacks/signed-token"
}Preview requests (POST /video/preview):
{
"blob_url": "https://s3.example.com/source?presigned...",
"preview_image_url": "https://s3.example.com/preview?presigned...",
"preview_image_variant_url": "https://s3.example.com/variant?presigned...",
"dimensions": "640x480",
"rotation": 0,
"callback_url": "https://app.example.com/active_storage/async_variants/callbacks/signed-token"
}Callbacks are handled by active_storage-async_variants, not this gem. The callback endpoint is auto-mounted at:
POST /active_storage/async_variants/callbacks/:token
Crucible must POST {"status": "success"} or {"status": "failed", "error": "..."} to this URL after processing. The token is signed -- no authentication headers are needed. The endpoint must be publicly reachable by the Crucible service.
MIT