mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
318 lines
13 KiB
Ruby
318 lines
13 KiB
Ruby
class BasilController < ApplicationController
|
|
before_action :authenticate_user!, except: [:complete_commission, :about, :stats]
|
|
|
|
before_action :require_admin_access, only: [:review], unless: -> { Rails.env.development? }
|
|
|
|
def index
|
|
@enabled_content_types = [Character].map(&:name)
|
|
|
|
@content_type = params[:content_type].try(:humanize) || 'Character'
|
|
if @content_type.nil? || !@enabled_content_types.include?(@content_type)
|
|
return raise "Invalid content type: #{params[:content_type]}"
|
|
end
|
|
|
|
@content = @current_user_content[@content_type]
|
|
end
|
|
|
|
def content
|
|
# Fetch the content page from our already-queried cache of current user content
|
|
@content_type = params[:content_type].humanize
|
|
@content = @current_user_content[@content_type].detect do |page|
|
|
page.id == params[:id].to_i
|
|
end
|
|
raise "No content found for #{params[:content_type]} with ID #{params[:id]} for user #{current_user.id}" if @content.nil?
|
|
|
|
# Fetch any existing Basil configurations/guidance for this character
|
|
@guidance = BasilFieldGuidance.find_or_initialize_by(entity: @content, user: current_user).try(:guidance)
|
|
@guidance ||= {}
|
|
|
|
# Fetch all the related fields for this content type and their values
|
|
# and format them into an array of [field, value] pairs to pass to the view
|
|
@relevant_fields = []
|
|
|
|
case @content_type
|
|
when 'Character'
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Overview', 'Gender')
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Overview', 'Age')
|
|
@relevant_fields.push BasilService.include_all_fields_in_category(current_user, @content, ['Looks', 'Appearance'])
|
|
|
|
when 'Location'
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Overview', 'Name')
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Overview', 'Type')
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Overview', 'Description')
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Geography', 'Area')
|
|
@relevant_fields.push BasilService.include_specific_field(current_user, @content, 'Geography', 'Climate')
|
|
|
|
end
|
|
@relevant_fields.compact!
|
|
|
|
# Finally, cache some state we can reference in the view
|
|
@commissions = BasilCommission.where(entity_type: @content.page_type, entity_id: @content.id)
|
|
.order('id DESC')
|
|
.limit(20)
|
|
.includes(:basil_feedbacks)
|
|
@in_progress_commissions = @commissions.select { |c| c.completed_at.nil? }
|
|
@can_request_another = @in_progress_commissions.count < 3
|
|
end
|
|
|
|
def character
|
|
@character = current_user.characters.find(params[:id])
|
|
@guidance = BasilFieldGuidance.find_or_initialize_by(entity: @character, user: current_user).try(:guidance)
|
|
@guidance ||= {}
|
|
|
|
category_ids = AttributeCategory.where(
|
|
user_id: current_user.id,
|
|
entity_type: 'character',
|
|
label: ['Looks', 'Appearance']
|
|
).pluck(:id)
|
|
@appearance_fields = AttributeField.where(attribute_category_id: category_ids)
|
|
@attributes = Attribute.where(
|
|
attribute_field_id: @appearance_fields.pluck(:id),
|
|
entity_id: @character.id,
|
|
entity_type: 'Character'
|
|
)
|
|
|
|
@commissions = BasilCommission.where(entity_type: 'Character', entity_id: @character.id)
|
|
.order('id DESC')
|
|
.limit(20)
|
|
.includes(:basil_feedbacks)
|
|
@in_progress_commissions = BasilCommission.where(entity_type: 'Character', entity_id: @character.id, completed_at: nil)
|
|
@can_request_another = @in_progress_commissions.count < 3
|
|
end
|
|
|
|
def about
|
|
end
|
|
|
|
def stats
|
|
@commissions = BasilCommission.all
|
|
|
|
@queued = BasilCommission.where(completed_at: nil)
|
|
@completed = BasilCommission.where.not(completed_at: nil)
|
|
|
|
@average_wait_time = @completed.where('completed_at > ?', 24.hours.ago)
|
|
.average(:cached_seconds_taken)
|
|
@seconds_over_time = @completed.where('completed_at > ?', 24.hours.ago)
|
|
.group_by { |c| ((c.cached_seconds_taken || 0) / 60).round }
|
|
.map { |minutes, list| [minutes, list.count] }
|
|
|
|
# Projected date, at our current rate, to reach 1,000,000 images
|
|
commission_counts_per_day = @commissions.group_by_day(:completed_at).values
|
|
@average_commissions_per_day = commission_counts_per_day.sum(0.0) / commission_counts_per_day.count
|
|
commissions_left = 1_000_000 - @commissions.count
|
|
@days_til_1_million_commissions = commissions_left / @average_commissions_per_day
|
|
|
|
# Feedback today
|
|
@feedback_today = BasilFeedback.where('updated_at > ?', 24.hours.ago)
|
|
.order(:score_adjustment)
|
|
.group(:score_adjustment)
|
|
.count
|
|
total = @feedback_today.values.sum
|
|
@emoji_counts_today = @feedback_today.map do |score, count|
|
|
emoji = case score
|
|
when -2 then "Very Bad :'("
|
|
when -1 then "Bad :("
|
|
when 0 then "Meh :|"
|
|
when 1 then "Good :)"
|
|
when 2 then "Very Good :D"
|
|
when 3 then "Lovely! <3"
|
|
end
|
|
[emoji, (count / total.to_f * 100).round(1)]
|
|
end
|
|
|
|
# Feedback all time
|
|
@feedback_before_today = BasilFeedback.where('updated_at < ?', 24.hours.ago)
|
|
.order(:score_adjustment)
|
|
.group(:score_adjustment)
|
|
.count
|
|
days_since_start = (Date.current - BasilFeedback.minimum(:updated_at).to_date)
|
|
days_since_start = 1 if days_since_start.zero? # no dividing by 0 lol
|
|
|
|
total = @feedback_before_today.values.sum
|
|
@emoji_counts_all_time = @feedback_before_today.map do |score, count|
|
|
emoji = case score
|
|
when -2 then "Very Bad :'("
|
|
when -1 then "Bad :("
|
|
when 0 then "Meh :|"
|
|
when 1 then "Good :)"
|
|
when 2 then "Very Good :D"
|
|
when 3 then "Lovely! <3"
|
|
end
|
|
|
|
[emoji, (count / total.to_f * 100).round(1)]
|
|
end
|
|
|
|
active_styles = %w(realistic painting sketch digital anime abstract painting2 horror watercolor)
|
|
@total_score_per_style = BasilCommission.where(style: active_styles)
|
|
.joins(:basil_feedbacks)
|
|
.group(:style)
|
|
.sum(:score_adjustment)
|
|
.map { |style, average| [style, average.round(1)] }
|
|
.sort_by(&:second)
|
|
.reverse
|
|
@average_score_per_style = BasilCommission.where(style: active_styles)
|
|
.joins(:basil_feedbacks)
|
|
.group(:style)
|
|
.average(:score_adjustment)
|
|
.map { |style, average| [style, average.round(1)] }
|
|
.sort_by(&:second)
|
|
.reverse
|
|
|
|
# queue size (total commissions - completed commissions)
|
|
# average time to complete today / this week
|
|
# commissions per day bar chart
|
|
# count(average time to complete) bar chart
|
|
end
|
|
|
|
def review
|
|
@recent_commissions = BasilCommission.all.includes(:entity, :user).order('id DESC').limit(100)
|
|
end
|
|
|
|
def commission
|
|
# Fetch the related content
|
|
@content = @current_user_content[commission_params.fetch(:entity_type)]
|
|
.find { |c| c.id == commission_params.fetch(:entity_id).to_i }
|
|
return raise "Invalid content commission params" if @content.nil?
|
|
|
|
# Before creating the prompt, do a little config to tweak things to work well :)
|
|
labels_to_omit_label_text = [
|
|
"Name",
|
|
"Identifying Marks",
|
|
"Type",
|
|
"Description"
|
|
].map(&:downcase)
|
|
field_importance_multipliers = {
|
|
'hair': 1.15,
|
|
'hair color': 1.55,
|
|
'hair style': 1.10,
|
|
'skin tone': 1.05,
|
|
'race': 1.10,
|
|
'eye color': 1.05,
|
|
'gender': 1.15
|
|
}
|
|
label_value_pairs_to_skip_entirely = [
|
|
['race', 'human']
|
|
]
|
|
value_suffix_for_numerical_fields = {
|
|
'age': ' years old'
|
|
}
|
|
|
|
# Prepare our prompt components
|
|
prompt_components = []
|
|
commission_params.fetch(:field).each do |field_id, field_data|
|
|
label = field_data[:label].strip
|
|
value = field_data[:value].gsub(',', '')
|
|
.gsub("\r", '')
|
|
.gsub('(', '')
|
|
.gsub(')', '')
|
|
.gsub("\n", ' ')
|
|
.strip
|
|
importance = field_data[:importance].to_f
|
|
|
|
# Field skips
|
|
next if label_value_pairs_to_skip_entirely.include?([label.downcase, value.downcase])
|
|
|
|
# Do any per-field manipulations
|
|
importance *= field_importance_multipliers[label.downcase.to_sym] if field_importance_multipliers.key?(label.downcase.to_sym)
|
|
value += value_suffix_for_numerical_fields[label.downcase.to_sym] if value_suffix_for_numerical_fields.key?(label.downcase.to_sym)
|
|
label = '' if labels_to_omit_label_text.include?(label.downcase)
|
|
|
|
# Finally, cut down on any unnecessary precision to save more tokens
|
|
importance = importance.round(2)
|
|
|
|
component_text = "#{value} #{label}".strip
|
|
if importance == 1.0
|
|
# If the importance is exactly 1, we can omit the parentheses and save a few tokens, since the
|
|
# default attention importance is 1.
|
|
prompt_components.push "#{component_text}"
|
|
elsif importance != 0
|
|
# If the importance isn't 1 (default) or 0 (not included), we want to specify it in the prompt.
|
|
prompt_components.push "(#{component_text}:#{importance})"
|
|
end
|
|
end
|
|
|
|
# Build a prompt for Basil from the component parts
|
|
prompt = prompt_components.join(' ')
|
|
|
|
# Save our field weights as the latest guidance also
|
|
guidance = BasilFieldGuidance.find_or_initialize_by(entity_type: @content.page_type,
|
|
entity_id: @content.id,
|
|
user: current_user)
|
|
guidance_data = commission_params.fetch(:field)
|
|
.transform_values { |data| data[:importance].to_f }
|
|
.to_h
|
|
guidance.update(guidance: guidance_data)
|
|
|
|
BasilCommission.create!(
|
|
user: current_user,
|
|
entity_type: @content.page_type,
|
|
entity_id: @content.id,
|
|
prompt: prompt,
|
|
style: commission_params.fetch(:style, 'realistic'),
|
|
job_id: SecureRandom.uuid
|
|
)
|
|
|
|
redirect_to basil_content_path(@content.page_type, @content.id)
|
|
end
|
|
|
|
def complete_commission
|
|
commission = BasilCommission.find_by(job_id: params[:jobid])
|
|
return if commission.nil?
|
|
|
|
commission.update(completed_at: DateTime.current,
|
|
final_settings: JSON.parse(params[:settings]))
|
|
|
|
commission.cache_after_complete!
|
|
|
|
# TODO: we should attach the S3 object to the commission.image attachment
|
|
# but I dunno how to do that yet. See broken attempts below.
|
|
|
|
# s3 = Aws::S3::Resource.new(region: "us-east-1")
|
|
# obj = s3.bucket("basil-characters").object("job-#{params[:jobid]}.png")
|
|
# blob_params = {
|
|
# filename: File.basename(obj.key),
|
|
# content_type: obj.content_type,
|
|
# byte_size: obj.size,
|
|
# checksum: obj.etag.gsub('"',"")
|
|
# }
|
|
# raise "wow"
|
|
# blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_params)
|
|
|
|
# # By default, the blob's key (S3 key, in this case) a secure (random) token
|
|
# # However, since the file is already on S3, we need to change the
|
|
# # key to match our file on S3
|
|
# blob.update_attribute :key, obj.key
|
|
|
|
# raise params.inspect
|
|
|
|
render json: { success: true }
|
|
end
|
|
|
|
def feedback
|
|
commission = BasilCommission.find_by(job_id: params[:jobid])
|
|
score_adjustment = params[:basil_feedback][:score_adjustment].to_i
|
|
score_adjustment = score_adjustment.clamp(-3, 3)
|
|
|
|
feedback = commission.basil_feedbacks.find_or_initialize_by(user: current_user)
|
|
feedback.update!(score_adjustment: score_adjustment)
|
|
end
|
|
|
|
def help_rate
|
|
# Commissions without feedback:
|
|
@reviewed_commission_ids = BasilFeedback.where(user: current_user)
|
|
.pluck(:basil_commission_id)
|
|
@commissions = BasilCommission.where.not(id: @reviewed_commission_ids)
|
|
.where.not(completed_at: nil)
|
|
.where(user: current_user)
|
|
.order(created_at: :desc)
|
|
.limit(50)
|
|
.includes(:entity)
|
|
.shuffle
|
|
end
|
|
|
|
private
|
|
|
|
def commission_params
|
|
params.require(:basil_commission).permit(:style, :entity_type, :entity_id, field: {})
|
|
end
|
|
end
|