# frozen_string_literal: true module DiscourseAi module AiBot module Tools class Image < Tool def self.signature { name: name, description: "Renders an image from the description (remove all connector words, keep it to 40 words or less). Despite being a text based bot you can generate images! (when user asks to draw, paint or other synonyms try this)", parameters: [ { name: "prompts", description: "The prompts used to generate or create or draw the image (40 words or less, be creative) up to 4 prompts", type: "array", item_type: "string", required: true, }, { name: "seeds", description: "The seed used to generate the image (optional) - can be used to retain image style on amended prompts", type: "array", item_type: "integer", }, { name: "aspect_ratio", description: "The aspect ratio of the image (optional defaults to 1:1)", type: "string", required: false, enum: %w[16:9 1:1 21:9 2:3 3:2 4:5 5:4 9:16 9:21], }, ], } end def self.name "image" end def initialize(*args, **kwargs) super @chain_next_response = false end def prompts parameters[:prompts] end def aspect_ratio parameters[:aspect_ratio] end def seeds parameters[:seeds] end def chain_next_response? @chain_next_response end def invoke # max 4 prompts selected_prompts = prompts.take(4) seeds = seeds.take(4) if seeds progress = prompts.first yield(progress) results = nil # this ensures multisite safety since background threads # generate the images api_key = SiteSetting.ai_stability_api_key engine = SiteSetting.ai_stability_engine api_url = SiteSetting.ai_stability_api_url threads = [] selected_prompts.each_with_index do |prompt, index| seed = seeds ? seeds[index] : nil threads << Thread.new(seed, prompt) do |inner_seed, inner_prompt| attempts = 0 begin DiscourseAi::Inference::StabilityGenerator.perform!( inner_prompt, engine: engine, api_key: api_key, api_url: api_url, image_count: 1, seed: inner_seed, aspect_ratio: aspect_ratio, ) rescue => e attempts += 1 retry if attempts < 3 Rails.logger.warn("Failed to generate image for prompt #{prompt}: #{e}") nil end end end break if threads.all? { |t| t.join(2) } while true results = threads.map(&:value).compact if !results.present? @chain_next_response = true return( { prompts: prompts, error: "Something went wrong inform user you could not generate image, check Discourse logs, give up don't try anymore", give_up: true, } ) end uploads = [] results.each_with_index do |result, index| result[:artifacts].each do |image| Tempfile.create("v1_txt2img_#{index}.png") do |file| file.binmode file.write(Base64.decode64(image[:base64])) file.rewind uploads << { prompt: prompts[index], upload: UploadCreator.new( file, "image.png", for_private_message: context[:private_message], ).create_for(bot_user.id), seed: image[:seed], } end end end @custom_raw = <<~RAW [grid] #{ uploads .map { |item| "![#{item[:prompt].gsub(/\|\'\"/, "")}](#{item[:upload].short_url})" } .join(" ") } [/grid] RAW { prompts: uploads.map { |item| item[:prompt] }, seeds: uploads.map { |item| item[:seed] }, } end protected def description_args { prompt: prompts.first } end end end end end