Skip to content

Commit 41b6b55

Browse files
authored
Merge pull request #158 from merefield/paint_aspect_fixes
FIX: image aspect ratio rendering and edits
2 parents 795c889 + d963003 commit 41b6b55

8 files changed

Lines changed: 270 additions & 126 deletions

File tree

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ en:
183183
creates a picture based on the provided direction and the last image in the conversation
184184
parameters:
185185
description: a description of the edit to the picture the user wants to make
186+
aspect_ratio: optional image shape override. if omitted, keep the original image aspect ratio
186187
no_image_error: there was no image in the conversation to edit
187188
error: there was an issue creating the new picture.
188189
forum_search:

lib/discourse_chatbot/functions/paint_edit_function.rb

Lines changed: 82 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# frozen_string_literal: true
22

3-
require_relative '../function'
3+
require_relative "../function"
44

55
module DiscourseChatbot
66
class PaintEditFunction < Function
7-
TOKEN_COST = 1000000 # 1M tokens per request based on cost of dall-e-3 model vs gpt-4o-mini
7+
TOKEN_COST = 1_000_000 # 1M tokens per request based on cost of dall-e-3 model vs gpt-4o-mini
88

99
def name
10-
'paint_edit_picture'
10+
"paint_edit_picture"
1111
end
1212

1313
def description
@@ -16,12 +16,22 @@ def description
1616

1717
def parameters
1818
[
19-
{ name: "description", type: String, description: I18n.t("chatbot.prompt.function.paint_edit.parameters.description") } ,
19+
{
20+
name: "description",
21+
type: String,
22+
description: I18n.t("chatbot.prompt.function.paint_edit.parameters.description"),
23+
},
24+
{
25+
name: "aspect_ratio",
26+
type: String,
27+
enum: PaintFunction::ASPECT_RATIO_OPTIONS,
28+
description: I18n.t("chatbot.prompt.function.paint_edit.parameters.aspect_ratio"),
29+
},
2030
]
2131
end
2232

2333
def required
24-
['description']
34+
["description"]
2535
end
2636

2737
def process(args, opts)
@@ -31,39 +41,49 @@ def process(args, opts)
3141

3242
description = args[parameters[0][:name]]
3343

34-
client = OpenAI::Client.new do |f|
35-
f.response :logger, Logger.new($stdout), bodies: true if SiteSetting.chatbot_enable_verbose_console_logging
36-
if SiteSetting.chatbot_enable_verbose_rails_logging != "off"
37-
case SiteSetting.chatbot_verbose_rails_logging_destination_level
44+
client =
45+
OpenAI::Client.new do |f|
46+
if SiteSetting.chatbot_enable_verbose_console_logging
47+
f.response :logger, Logger.new($stdout), bodies: true
48+
end
49+
if SiteSetting.chatbot_enable_verbose_rails_logging != "off"
50+
case SiteSetting.chatbot_verbose_rails_logging_destination_level
3851
when "warn"
3952
f.response :logger, Rails.logger, bodies: true, log_level: :warn
4053
else
4154
f.response :logger, Rails.logger, bodies: true, log_level: :info
55+
end
4256
end
4357
end
58+
59+
type = opts[:type]
60+
61+
last_image_upload =
62+
(
63+
if type == ::DiscourseChatbot::POST
64+
last_post_image_upload(opts[:reply_to_message_or_post_id])
65+
else
66+
last_message_image_upload(opts[:reply_to_message_or_post_id])
67+
end
68+
)
69+
70+
if last_image_upload.nil?
71+
return(
72+
{ answer: I18n.t("chatbot.prompt.function.paint_edit.no_image_error"), token_usage: 0 }
73+
)
4474
end
4575

46-
size = "1536x1024"
76+
aspect_ratio = resolved_aspect_ratio(args[parameters[1][:name]], last_image_upload)
77+
size =
78+
PaintFunction.size_for(SiteSetting.chatbot_support_picture_creation_model, aspect_ratio)
4779
quality = "auto"
4880

4981
options = {
5082
model: SiteSetting.chatbot_support_picture_creation_model,
5183
prompt: description,
5284
size: size,
5385
quality: quality,
54-
}
55-
56-
type = opts[:type]
57-
58-
last_image_upload = type == ::DiscourseChatbot::POST ? last_post_image_upload(opts[:reply_to_message_or_post_id]) : last_message_image_upload(opts[:reply_to_message_or_post_id])
59-
60-
return {
61-
answer: I18n.t("chatbot.prompt.function.paint_edit.no_image_error"),
62-
token_usage: 0
63-
} if last_image_upload.nil?
64-
65-
file_path = path = Discourse.store.path_for(last_image_upload)
66-
base64_encoded_data = Base64.strict_encode64(File.read(file_path))
86+
}
6787

6888
file_path = Discourse.store.path_for(last_image_upload)
6989
extension = last_image_upload.extension
@@ -73,16 +93,17 @@ def process(args, opts)
7393
f.binmode
7494
f.write(File.binread(file_path))
7595
f.rewind
76-
96+
7797
# Specify the file with MIME type
7898
upload_io = Faraday::Multipart::FilePart.new(f, mime_type)
79-
99+
80100
options[:image] = upload_io
81-
82-
response = client.images.edit(parameters: options)
83-
84-
f.close
85-
f.unlink
101+
begin
102+
response = client.images.edit(parameters: options)
103+
ensure
104+
f.close
105+
f.unlink
106+
end
86107

87108
if response.dig("error")
88109
error_text = "ERROR when trying to call paint API: #{response.dig("error", "message")}"
@@ -91,32 +112,30 @@ def process(args, opts)
91112

92113
tokens_used = response.dig("usage", "total_tokens")
93114

94-
artifacts = response.dig("data")
95-
.to_a
96-
.map { |art| art["b64_json"] }
115+
artifacts = response.dig("data").to_a.map { |art| art["b64_json"] }
97116

98117
bot_username = SiteSetting.chatbot_bot_user
99118
bot_user = ::User.find_by(username: bot_username)
100119

101120
thumbnails = base64_to_image(artifacts, description, bot_user.id)
102-
short_url = thumbnails.first.short_url
103-
markdown = "![#{description}|690x460](#{short_url})"
104-
105-
{
106-
answer: markdown,
107-
token_usage: tokens_used
108-
}
121+
markdown =
122+
PaintFunction.markdown_for(
123+
upload: thumbnails.first,
124+
description: description,
125+
fallback_size: size,
126+
)
127+
128+
{ answer: markdown, token_usage: tokens_used }
109129
rescue => e
110130
Rails.logger.error("Chatbot: Error in paint edit function: #{e}")
111131
if e.respond_to?(:response)
112132
status = e.response[:status]
113133
message = e.response[:body]["error"]["message"]
114-
Rails.logger.error("Chatbot: There was a problem with Image call: status: #{status}, message: #{message}")
134+
Rails.logger.error(
135+
"Chatbot: There was a problem with Image call: status: #{status}, message: #{message}",
136+
)
115137
end
116-
{
117-
answer: I18n.t("chatbot.prompt.function.paint_edit.error"),
118-
token_usage: TOKEN_COST
119-
}
138+
{ answer: I18n.t("chatbot.prompt.function.paint_edit.error"), token_usage: TOKEN_COST }
120139
end
121140
end
122141

@@ -130,34 +149,39 @@ def base64_to_image(artifacts, description, user_id)
130149
f.binmode
131150
f.write(Base64.decode64(art))
132151
f.rewind
133-
upload = UploadCreator.new(f, attribution).create_for(user_id)
134-
f.unlink
152+
begin
153+
upload = UploadCreator.new(f, attribution).create_for(user_id)
154+
ensure
155+
f.close
156+
f.unlink
157+
end
135158

136-
UploadSerializer.new(upload, root: false)
159+
upload
137160
end
138161
end
139162

163+
def resolved_aspect_ratio(aspect_ratio, upload)
164+
return PaintFunction.normalized_aspect_ratio(aspect_ratio) if aspect_ratio.present?
165+
166+
PaintFunction.aspect_ratio_for_upload(upload)
167+
end
168+
140169
def last_post_image_upload(post_id)
141170
post_collection = ::DiscourseChatbot::PostPromptUtils.collect_past_interactions(post_id)
142171

143172
return nil if post_collection.empty?
144-
173+
145174
upload_id = post_collection.map(&:image_upload_id).compact.max
146-
return Upload.find_by(id: upload_id)
147-
148-
nil
175+
Upload.find_by(id: upload_id)
149176
end
150177

151178
def last_message_image_upload(message_id)
152-
message_collection = ::DiscourseChatbot::MessagePromptUtils.collect_past_interactions(message_id)
179+
message_collection =
180+
::DiscourseChatbot::MessagePromptUtils.collect_past_interactions(message_id)
153181
uploads = []
154182

155183
message_collection.each do |cm|
156-
cm.uploads.each do |ul|
157-
if %w[png webp jpg jpeg].include?(ul.extension)
158-
uploads << ul
159-
end
160-
end
184+
cm.uploads.each { |ul| uploads << ul if %w[png webp jpg jpeg].include?(ul.extension) }
161185
end
162186

163187
return nil if uploads.empty?

0 commit comments

Comments
 (0)