diff --git a/.envrc.sample b/.envrc.sample index 623037a..6ae45df 100644 --- a/.envrc.sample +++ b/.envrc.sample @@ -7,6 +7,9 @@ export AWS_REGION="us-east-1" export S3_ENDPOINT=http://docker.for.mac.localhost:9000 # Do not set if connecting to the real AWS S3 bucket export S3_BUCKET_NAME=pdf_accessibility_api # Default value for MinIO in the docker compose environment +# Alt Text Gem Info +export LLM_MODEL=default + #--------------------------------- # The below configurations are not # needed if using docker compose diff --git a/Dockerfile b/Dockerfile index 8fe3c1d..9fa8219 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,7 @@ RUN RAILS_ENV=production \ AWS_ACCESS_KEY_ID=key \ AWS_SECRET_ACCESS_KEY=secret \ AWS_REGION=us-east-1 \ + LLM_MODEL=default \ bundle exec rails assets:precompile && \ rm -rf /app/.cache/ && \ rm -rf /app/node_modules/.cache/ && \ diff --git a/README.md b/README.md index 0eb24ed..0aae746 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ At its core, the PDF Accessibility API is an interface to an S3 bucket with: - an input directory, where the API places files to be processed by the PDF_Accessibility application - an output directory, where the PDF_Accessibility application places the processed files to be retrieved -The PDF Accessibility API acts as an intermediary to send and retrieve those files for clients. It has two major components: the API and the GUI. +The PDF Accessibility API acts as an intermediary to send and retrieve those files for clients. It has two major components: the API and the GUI. Additionally, there is the option to only generate alt-text for a given image. This option is currently only available through a GUI. -## API +## PDF Remediation – API Refer to the Swagger documentation for endpoint and webhook details at `/api-docs`. @@ -29,15 +29,21 @@ We use an `APIUser` model to store metadata for our API users and their associat - The client's `webhook_key` for authenticating with the client system when the final webhook request is sent. - An `email` and `name` to help identify the user. -## GUI +## PDF Remediation - GUI -The GUI is still a work in progress, but its main components are: +The PDF Remediation GUI's main components are: -- `/jobs` — a list of your jobs. -- `/jobs/new` — the page for uploading a file to remediate. -- `/jobs/{id}` — detailed information about a job (linked from `/jobs`). +- `/pdf_jobs` — a list of your jobs. +- `/pdf_jobs/new` — the page for uploading a file to remediate. +- `/pdf_jobs/{id}` — detailed information about a job (linked from `/pdf_jobs`). - `/sidekiq` — Sidekiq interface. +## Image Alt Text - GUI +There is also a standalone GUI just for images. This is for users who just want to generate alt-text for an image without going through the full - and pricy - PDF remediation process. +- `/image_jobs` — a list of image jobs, their links, and their status. +- `/image_jobs/new` — the upload page for a new image +- `/image_jobs/{id}` — detailed information about an image, including any generated alt-text. + ### Authentication and Authorization - The application uses a remote user header (default: `HTTP_X_AUTH_REQUEST_EMAIL`) to determine the current user, typically set by Azure. diff --git a/app/controllers/image_jobs_controller.rb b/app/controllers/image_jobs_controller.rb index f1e1bcd..cebc9d5 100644 --- a/app/controllers/image_jobs_controller.rb +++ b/app/controllers/image_jobs_controller.rb @@ -15,15 +15,18 @@ def new end def create - uploaded_io = params[:image] - object_key = "#{SecureRandom.hex(8)}_#{uploaded_io.original_filename}" + uploads_tmp_dir = Rails.root.join('tmp/uploads') + uploaded_file = params[:image] + object_key = "#{SecureRandom.uuid}_#{uploaded_file.original_filename}" + tmp_path = uploads_tmp_dir.join(object_key).to_s + File.binwrite(tmp_path, uploaded_file.read) job = current_user.image_jobs.build - job.output_object_key = object_key + job.output_object_key = uploaded_file.original_filename job.status = 'processing' job.uuid = SecureRandom.uuid job.save! - ImageAltTextJob.perform_later(job.uuid, uploaded_io.to_json) + ImageAltTextJob.perform_later(job.uuid, tmp_path) render json: { 'jobId' => job.id } end end diff --git a/app/javascript/controllers/job_controller.js b/app/javascript/controllers/job_controller.js index 3df719b..6d8438e 100644 --- a/app/javascript/controllers/job_controller.js +++ b/app/javascript/controllers/job_controller.js @@ -6,6 +6,7 @@ export default class extends Controller { static targets = ['outputObjectKey', 'status', 'finishedAt', + 'altText', 'downloadLink', 'processingErrorMessage']; @@ -32,6 +33,7 @@ export default class extends Controller { this.data.set('outputUrl', data.output_url || '') this.data.set('outputUrlExpired', data.output_url_expired || 'false') this.data.set('processingErrorMessage', data.processing_error_message || '') + this.data.set('altText', data.alt_text || '') this.renderResult() } diff --git a/app/jobs/api_remediation_job.rb b/app/jobs/api_remediation_job.rb index 4addb7a..cfc059e 100644 --- a/app/jobs/api_remediation_job.rb +++ b/app/jobs/api_remediation_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class APIRemediationJob < ApplicationJob - include RemediationModule + include AppJobModule def perform(job_uuid, output_polling_timeout: OUTPUT_POLLING_TIMEOUT) job = PdfJob.find_by!(uuid: job_uuid) @@ -13,10 +13,10 @@ def perform(job_uuid, output_polling_timeout: OUTPUT_POLLING_TIMEOUT) s3.upload_to_input(file_path) poll_and_update(job_uuid, object_key, output_polling_timeout) rescue S3Handler::Error => e - record_failure_and_notify(job, "Failed to upload file to remediation input location: #{e.message}") + update_with_failure(job, "Failed to upload file to remediation input location: #{e.message}") rescue Down::Error => e # We may want to retry the download depending on the more specific nature of the failure. - record_failure_and_notify(job, "Failed to download file from source URL: #{e.message}") + update_with_failure(job, "Failed to download file from source URL: #{e.message}") ensure RemediationStatusNotificationJob.perform_later(job_uuid) tempfile&.close! diff --git a/app/jobs/concerns/remediation_module.rb b/app/jobs/concerns/app_job_module.rb similarity index 83% rename from app/jobs/concerns/remediation_module.rb rename to app/jobs/concerns/app_job_module.rb index f30ec47..a183109 100644 --- a/app/jobs/concerns/remediation_module.rb +++ b/app/jobs/concerns/app_job_module.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module RemediationModule +module AppJobModule OUTPUT_POLLING_INTERVAL = 10 # This value was picked somewhat arbitrarily. We may want to adjust. OUTPUT_POLLING_TIMEOUT = 3600 # The default 1-hour timeout is also arbitrary and should probably be adjusted. PRESIGNED_URL_EXPIRES_IN = 84_000 @@ -15,14 +15,14 @@ def poll_and_update(job_uuid, object_key, output_polling_timeout) timer += OUTPUT_POLLING_INTERVAL if timer > output_polling_timeout - record_failure_and_notify(job, 'Timed out waiting for output file') + update_with_failure(job, 'Timed out waiting for output file') return true end end update_job(job, output_url, object_key) rescue S3Handler::Error => e # We may want to retry the upload depending on the more specific nature of the failure. - record_failure_and_notify(job, "Failed to upload file to remediation input location: #{e.message}") + update_with_failure(job, "Failed to upload file to remediation input location: #{e.message}") end private @@ -37,7 +37,7 @@ def update_job(job, output_url, object_key) ) end - def record_failure_and_notify(job, message) + def update_with_failure(job, message) job.update( status: 'failed', finished_at: Time.zone.now, diff --git a/app/jobs/gui_remediation_job.rb b/app/jobs/gui_remediation_job.rb index d3bf2d3..b021b11 100644 --- a/app/jobs/gui_remediation_job.rb +++ b/app/jobs/gui_remediation_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class GUIRemediationJob < ApplicationJob - include RemediationModule + include AppJobModule def perform(job_uuid, object_key, output_polling_timeout: OUTPUT_POLLING_TIMEOUT) poll_and_update(job_uuid, object_key, output_polling_timeout) diff --git a/app/jobs/image_alt_text_job.rb b/app/jobs/image_alt_text_job.rb index fc22bcd..ff61957 100644 --- a/app/jobs/image_alt_text_job.rb +++ b/app/jobs/image_alt_text_job.rb @@ -1,11 +1,28 @@ # frozen_string_literal: true class ImageAltTextJob < ApplicationJob - def perform(job_uuid, uploaded_io, output_polling_timeout: OUTPUT_POLLING_TIMEOUT) - # To be implemented in #159 - # Open the file file in a temp/uploads path - # Call AltTextGem with path, prompt, llm_model - # Poll and reroute - # File.delete(tmp_path) if File.exist?(tmp_path) + include AppJobModule + + def perform(job_uuid, tmp_path) + client = AltText::Client.new( + access_key: ENV.fetch('AWS_ACCESS_KEY_ID', nil), + secret_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil), + region: ENV.fetch('AWS_REGION', 'us-east-1') + ) + job = Job.find_by!(uuid: job_uuid) + alt_text = client.process_image( + tmp_path, + prompt: Rails.root.join('prompt.txt').read, + model_id: ENV.fetch('LLM_MODEL', 'nil') + ) + job.update( + status: 'completed', + finished_at: Time.zone.now, + alt_text: alt_text + ) + rescue StandardError => e + update_with_failure(job, e.message) + ensure + FileUtils.rm_f(tmp_path) end end diff --git a/app/models/upload_form.rb b/app/models/upload_form.rb deleted file mode 100644 index 4ac2e9f..0000000 --- a/app/models/upload_form.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class UploadForm - UPLOADS_TMP_DIR = Rails.root.join('tmp/uploads') - - include ActiveModel::Validations - include ActiveModel::Model - - attr_accessor :file - - validates :file, presence: true - - def persist_to_tmp! - return unless valid? - - tmp_path = UPLOADS_TMP_DIR.join("#{SecureRandom.uuid}_#{file.original_filename}") - File.binwrite(tmp_path, file.read) - file.rewind if file.respond_to?(:rewind) - tmp_path.to_s - end -end diff --git a/app/views/image_jobs/show.html.erb b/app/views/image_jobs/show.html.erb index 5c16f8c..1d90244 100644 --- a/app/views/image_jobs/show.html.erb +++ b/app/views/image_jobs/show.html.erb @@ -10,12 +10,12 @@ data-job-alt-text="<%= @image_job.alt_text %>" data-job-processing-error-message="<%= @image_job.processing_error_message %>" id="<%= dom_id @image_job %>"> -