1+ require 'net/http'
2+ require 'uri'
3+ require 'json'
4+ require 'base64'
5+ require 'tempfile'
6+ require 'aws-sdk-s3'
7+
8+ class GenerateBasilImageJob < ApplicationJob
9+ queue_as :default
10+
11+ # Define potential errors for rescue
12+ class ApiError < StandardError ; end
13+
14+ def perform ( basil_commission_id )
15+ # Find the BasilCommission record
16+ commission = BasilCommission . find ( basil_commission_id )
17+
18+ # Skip if already completed (image attached)
19+ return if commission . image . attached?
20+
21+ # Details we can use:
22+ # commission.prompt - the prompt for the image
23+ # commission.style - the style of the image
24+ # commission.entity_type - the type of entity the image is for (e.g. Character, Location, etc)
25+ prompt_to_send = "(#{ commission . style } style), #{ commission . entity_type } , #{ commission . prompt } "
26+
27+ # Make the API request to generate the image
28+ endpoint_url = ENV . fetch ( "BASIL_ENDPOINT" )
29+ api_url = URI . join ( endpoint_url , '/sdapi/v1/txt2img' )
30+
31+ puts "*" * 100
32+ puts "Prompt: #{ prompt_to_send } "
33+ puts "*" * 100
34+
35+ payload = {
36+ prompt : prompt_to_send ,
37+ steps : 20 ,
38+ # Add other parameters like negative_prompt, width, height, sampler_index, etc. as needed
39+ # Example:
40+ negative_prompt : "nudity, nsfw, nude, xxx, low quality, blurry, worst quality, diptych, triptych, multiple images, multiple subjects" ,
41+ width : 512 ,
42+ height : 512 ,
43+ sd_model_checkpoint : "openxl"
44+ } . to_json
45+
46+ begin
47+ response = Net ::HTTP . post ( api_url , payload , "Content-Type" => "application/json" )
48+ response . value # Raises an HTTPError if the response is not 2xx
49+
50+ response_data = JSON . parse ( response . body )
51+ image_data_base64 = response_data [ 'images' ] &.first
52+
53+ raise ApiError , "No image data found in API response" unless image_data_base64
54+
55+ # Decode the base64 image data
56+ image_data_binary = Base64 . decode64 ( image_data_base64 )
57+
58+ # --- Manual S3 Upload and ActiveStorage Blob Creation ---
59+ begin
60+ s3_client = Aws ::S3 ::Client . new (
61+ region : ENV . fetch ( 'AWS_REGION' , 'us-east-1' ) ,
62+ access_key_id : ENV . fetch ( 'AWS_ACCESS_KEY_ID' ) ,
63+ secret_access_key : ENV . fetch ( 'AWS_SECRET_ACCESS_KEY' )
64+ )
65+ bucket_name = ENV . fetch ( 'S3_BASIL_BUCKET_NAME' , 'basil-commissions' )
66+ s3_key = "job-#{ commission . job_id || SecureRandom . uuid } .png" # Use job_id for the key
67+ filename = s3_key # Use the same for the filename
68+
69+ # 1. Upload directly to S3
70+ Rails . logger . info "Uploading key '#{ s3_key } ' to bucket '#{ bucket_name } '"
71+ upload_response = s3_client . put_object (
72+ bucket : bucket_name ,
73+ key : s3_key ,
74+ body : image_data_binary ,
75+ content_type : 'image/png'
76+ # acl: 'public-read' # Only if you wanted public files, which we don't
77+ )
78+
79+ # 2. Create the ActiveStorage Blob record manually
80+ checksum = upload_response . etag . gsub ( '"' , '' ) # ETag comes with quotes
81+ byte_size = image_data_binary . size
82+
83+ blob = ActiveStorage ::Blob . create! (
84+ key : s3_key ,
85+ filename : filename ,
86+ content_type : 'image/png' ,
87+ byte_size : byte_size ,
88+ checksum : checksum ,
89+ service_name : :amazon_basil # Crucial: Specify the service!
90+ )
91+
92+ # 3. Associate the blob with the commission
93+ # Note: We use update! which saves immediately. No separate save! needed.
94+ commission . update! ( image : blob )
95+
96+ # 4. Update completed_at timestamp
97+ commission . update! ( completed_at : Time . current )
98+
99+ rescue Aws ::S3 ::Errors ::ServiceError => e
100+ Rails . logger . error "Manual S3 Upload/Blob Creation Failed: #{ e . class } - #{ e . message } "
101+ # Re-raise to let the job runner handle retries/failure
102+ raise e
103+ rescue ActiveRecord ::RecordInvalid => e
104+ Rails . logger . error "Manual Blob Creation/Association Failed: #{ e . class } - #{ e . message } "
105+ # Re-raise
106+ raise e
107+ end
108+
109+ rescue Net ::HTTPError , Net ::OpenTimeout , Net ::ReadTimeout , ApiError , JSON ::ParserError => e
110+ # Handle API errors, timeouts, or decoding issues
111+ # Log the error, potentially retry the job, or mark the commission as failed
112+ Rails . logger . error ( "Basil Image Generation Failed for commission #{ commission . id } : #{ e . message } " )
113+ # Example: Mark as failed (requires adding a status field to BasilCommission)
114+ # commission.update(status: 'failed', error_message: e.message)
115+ # Or re-raise to let the job runner handle retries/failure
116+ raise e
117+ end
118+ end
119+ end
0 commit comments