allow setting per-field weights for basil

This commit is contained in:
Andrew Brown 2023-02-26 16:44:02 -08:00
parent f84383623e
commit 4e42b055f7
5 changed files with 280 additions and 186 deletions

View File

@ -22,11 +22,11 @@ class BasilController < ApplicationController
entity_type: 'Character'
)
gender_field = @character.overview_field('Gender')
@gender_value = Attribute.find_by(attribute_field_id: gender_field.id, entity: @character).try(:value)
@gender_field = @character.overview_field('Gender')
@gender_value = Attribute.find_by(attribute_field_id: @gender_field.id, entity: @character).try(:value)
age_field = @character.overview_field('Age')
@age_value = Attribute.find_by(attribute_field_id: age_field.id, entity: @character).try(:value)
@age_field = @character.overview_field('Age')
@age_value = Attribute.find_by(attribute_field_id: @age_field.id, entity: @character).try(:value)
@commissions = BasilCommission.where(entity_type: 'Character', entity_id: @character.id).order('id DESC')
@can_request_another = @commissions.all? { |c| c.complete? }
@ -44,42 +44,97 @@ class BasilController < ApplicationController
@recent_commissions = BasilCommission.all.includes(:entity, :user).order('id DESC').limit(500)
end
def commission_character
def commission
# TODO: when we support multiple page types, we'll want to grab params[:entity_type] and params[:entity_id]
# and constantize the former, then find the entity from the user's content
@character = current_user.characters.find(params[:id])
# Build the prompt
category_ids = AttributeCategory.where(
user_id: current_user.id, entity_type: 'character', label: ['Looks', 'Appearance']
).pluck(:id)
# Build the prompt from the character's attributes
category_ids = AttributeCategory.where(user: current_user, 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'
)
attributes = Attribute.where(attribute_field_id: appearance_fields.pluck(:id),
entity_id: @character.id,
entity_type: 'Character')
prompt_components = []
# Step 1. Gender
gender_field = @character.overview_field('Gender')
gender_value = Attribute.find_by(attribute_field_id: gender_field.id, entity: @character).try(:value)
if gender_value.present?
gender_importance = params.dig(:field, gender_field.id.to_s)
# Add 1 because we present weight to the user as -1 to 1, but it's really 0 to 2 for Stable Diffusion.
if gender_importance.present?
gender_importance = gender_importance.to_f + 1
end
if gender_importance == 1
# 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 gender_value
elsif gender_importance != 0
# We also want to skip adding gender to the prompt at all if the user marked it as completely unimportant (-1 + 1 = 0)
prompt_components.push "(#{gender_value}:#{gender_importance})"
end
end
# Step 2. Age
age_field = @character.overview_field('Age')
age_value = Attribute.find_by(attribute_field_id: age_field.id, entity: @character).try(:value)
if age_value.present? && age_value.to_i.to_s == age_value
age_value = "#{age_value} years old"
if age_value.present?
# If the user simply entered a number in for an age field, we want to help SD along by
# giving it some context. Otherwise, we'll just use the value as-is.
if age_value.to_i.to_s == age_value
age_value = "#{age_value} years old"
end
age_importance = params.dig(:field, age_field.id.to_s)
# Add 1 because we present weight to the user as -1 to 1, but it's really 0 to 2 for Stable Diffusion.
if age_importance.present?
age_importance = age_importance.to_f + 1
end
if age_importance == 1
prompt_components.push age_value
elsif age_importance != 0
# We also want to skip adding gender to the prompt at all if the user marked it as completely unimportant (-1 + 1 = 0)
prompt_components.push "(#{age_value}:#{age_importance})"
end
end
# Step 3. Do it all again for every other field, too
formatted_field_values = appearance_fields.map do |field|
value = attributes.detect { |a| a.attribute_field_id == field.id }.try(:value)
# If there is no value to this field (or looks like it doesn't apply), skip it.
next if value.nil? || value.blank? || ['none', 'n/a', 'no', '.', '-', ' '].include?(value.try(:downcase))
# If the field is something implied like a "Human" answer on "Race", skip it.
next if field.label.downcase == 'race' && value.downcase == 'human'
"(#{value.gsub(',', '').gsub("\r", "").gsub("\n", " ")} #{field.label})"
# Get the importance of this field and add 1 to get back to our SD version
importance = params.dig(:field, field.id.to_s)
importance = importance.to_f + 1 if importance.present?
# If the importance is exactly 1, we can omit the parentheses and save a few tokens, since the
# default attention importance is 1.
if importance == 1
"#{value.gsub(',', '').gsub("\r", "").gsub("\n", " ")} #{field.label}"
elsif importance != 0
# We also want to skip adding gender to the prompt at all if the user marked it as completely unimportant (-1 + 1 = 0)
"(#{value.gsub(',', '').gsub("\r", "").gsub("\n", " ")} #{field.label}:#{importance})"
else
# For 0-importance fields, we'll compact them out of the list in a moment
nil
end
end
prompt = [
gender_value,
age_value,
formatted_field_values.compact.join(', '),
].compact.join(', ')
prompt_components.concat formatted_field_values.compact
prompt = prompt_components.join(', ')
BasilCommission.create!(
user: current_user,

View File

@ -0,0 +1,3 @@
class BasilService < Service
# TODO
end

View File

@ -1,180 +1,212 @@
<div class="row">
<div class="col s5">
<div>
<%= image_tag @character.random_image_including_private(format: :medium), style: 'width: 100%' %>
<h1 style="font-size: 2rem"><%= @character.name %></h1>
</div>
<%= form_for BasilCommission.new, url: basil_character_path(@character) do |f| %>
<%= f.hidden_field :style, value: 'realistic' %>
<%= f.hidden_field :entity_type, value: 'Character' %>
<%= f.hidden_field :entity_id, value: @character.id %>
<ul>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
Gender
</div>
<div>
<%= @gender_value %>
</div>
</li>
<div class="row">
<div class="col s5">
<div>
<%= image_tag @character.random_image_including_private(format: :medium), style: 'width: 100%' %>
<h1 style="font-size: 2rem"><%= @character.name %></h1>
</div>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
Age
</div>
<div>
<%= @age_value %>
</div>
</li>
<% shown_any_value = false %>
<% @appearance_fields.each do |field| %>
<%
value = @attributes.detect { |attr| attr.attribute_field_id == field.id }.try(:value)
next if value.nil? || value.blank?
shown_any_value = true
%>
<li style="margin-bottom: 1em;">
<ul>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
<%= field.label %>
Gender
<%= range_field_tag "field[#{@gender_field.id}]", 0, { min: -1, max: 1, step: 0.25, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div>
<%= value %>
<%= @gender_value %>
</div>
</li>
<% end %>
</ul>
<% if !shown_any_value %>
<div class="red card-panel lighten-3">
Basil works best with guidance! Please fill out some fields in the "Looks" category for this character
before requesting an image.
</div>
<% end %>
<li style="margin-bottom: 1em">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
Age
<%= range_field_tag "field[#{@age_field.id}]", 0, { min: -1, max: 1, step: 0.25, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div>
<%= @age_value %>
</div>
</li>
<div>
<%= link_to 'Edit this character page', edit_polymorphic_path(@character), class: 'grey-text text-darken-2' %>
</div>
<div>
<%= link_to 'Back to my other characters', basil_path, class: 'grey-text text-darken-2' %>
</div>
</div>
<div class="col s7">
<div class="card-panel" style="margin-top: 2em">
<p>
Basil is learning as fast as he can. Here are some tips and guidelines to get the most out of
him right now:
</p>
<p>
This is still a work in progress and very much a beta that will change a lot before releasing publicly,
but feel free to use it as much as you'd like to provide feedback!
</p>
<p>
If you run into any issues, please let me know <%# in the %>
<%# link_to 'Site Support forums', 'https://www.notebook.ai/forum/site-support' %>
<%# or %> <%= link_to 'on Discord', 'https://discord.gg/7WCuGxY3AW' %>.
</p>
</div>
<% if @can_request_another && shown_any_value %>
<div class="center">
To request Basil create an image of <%= @character.name %>, choose your desired style.
</div>
<div class="row">
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'realistic') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">photo</i>
Photograph
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'painting') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">palette</i>
Painting
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'sketch') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">edit</i>
Sketch
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'digital') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">brush</i>
Digital art
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'anime') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">face</i>
Anime
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to basil_commission_character_path(@character, style: 'abstract') do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">supervised_user_circle</i>
Abstract
</div>
<% end %>
</div>
</div>
<% end %>
<% @commissions.each do |commission| %>
<div>
<% if commission.complete? %>
<%# image_tag commission.image, style: 'width: 100%' %>
<% shown_any_value = false %>
<% @appearance_fields.each do |field| %>
<%
s3 = Aws::S3::Resource.new(region: "us-east-1")
obj = s3.bucket("basil-characters").object("job-#{commission.job_id}.png")
value = @attributes.detect { |attr| attr.attribute_field_id == field.id }.try(:value)
next if value.nil? || value.blank?
shown_any_value = true
%>
<div class="card horizontal">
<div class="card-image">
<%= link_to obj.presigned_url(:get) do %>
<%= image_tag obj.presigned_url(:get) %>
<% end %>
<li style="margin-bottom: 1em;">
<div class="grey-text text-darken-3" style="font-weight: bold; font-size: 0.8em">
<%= field.label %>
<%= range_field_tag "field[#{field.id}]", 0, { min: -1, max: 1, step: 0.25, style: 'width: 50%', class: 'js-importance-slider hide' } %>
</div>
<div class="card-stacked">
<div class="card-content">
<strong><%= @character.name %></strong>
<ul>
<li>
Completed <%= time_ago_in_words commission.completed_at %> ago
</li>
<li>
Took <%= distance_of_time_in_words commission.completed_at - commission.created_at %>
</li>
<hr />
<li>
You can click on the image to download it. Congratulations, it's yours now!
Feel free to upload it to your character's page if you want to keep and/or use it.
</li>
</ul>
<div>
<%= value %>
</div>
</li>
<% end %>
</ul>
<% if !shown_any_value %>
<div class="red card-panel lighten-3">
Basil works best with guidance! Please fill out some fields in the "Looks" category for this character
before requesting an image.
</div>
<% end %>
<div>
<%= link_to 'Customize per-field importance', "javascript:var sliders = document.getElementsByClassName('js-importance-slider'); for(var i = 0; i < sliders.length; i++) sliders.item(i).classList.remove('hide')" %>
<div class="card-panel js-importance-slider hide" style="margin-right: 1em">
<strong>How to customize per-field importance</strong>
<br /><br />
This allows you to tell Basil which fields are more or less important to you. For example, if Basil isn't
getting a character's hair color right, you can increase the importance of the "Hair Color" field.
<br /><br />
You can also tell Basil to ignore a field entirely by dragging the slider all the way to the left.
</div>
</div>
<br /><br />
<div>
<%= link_to 'Edit this character page', edit_polymorphic_path(@character), class: 'grey-text text-darken-2' %>
</div>
<div>
<%= link_to 'Back to my other characters', basil_path, class: 'grey-text text-darken-2' %>
</div>
</div>
<div class="col s7">
<div class="card-panel" style="margin-top: 2em">
<p>
Basil is learning as fast as he can. Here are some tips and guidelines to get the most out of
him right now:
</p>
<p>
This is still a work in progress and very much a beta that will change a lot before releasing publicly,
but feel free to use it as much as you'd like to provide feedback!
</p>
<p>
If you run into any issues, please let me know <%# in the %>
<%# link_to 'Site Support forums', 'https://www.notebook.ai/forum/site-support' %>
<%# or %> <%= link_to 'on Discord', 'https://discord.gg/7WCuGxY3AW' %>.
</p>
</div>
<% if @can_request_another && shown_any_value %>
<div class="center">
To request Basil create an image of <%= @character.name %>, choose your desired style.
</div>
<div class="row">
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("realistic")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">photo</i>
Photograph
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("painting")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">palette</i>
Painting
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("sketch")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">edit</i>
Sketch
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("digital")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">brush</i>
Digital art
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("anime")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">face</i>
Anime
</div>
<% end %>
</div>
<div class="col s12 m4 l4">
<%= link_to 'javascript:commission_basil("abstract")' do %>
<div class="hoverable card-panel blue white-text">
<i class="material-icons left">supervised_user_circle</i>
Abstract
</div>
<% end %>
</div>
</div>
<% end %>
<% @commissions.each do |commission| %>
<div>
<% if commission.complete? %>
<%# image_tag commission.image, style: 'width: 100%' %>
<%
s3 = Aws::S3::Resource.new(region: "us-east-1")
obj = s3.bucket("basil-characters").object("job-#{commission.job_id}.png")
%>
<div class="card horizontal">
<div class="card-image">
<%= link_to obj.presigned_url(:get) do %>
<%= image_tag obj.presigned_url(:get) %>
<% end %>
</div>
<div class="card-stacked">
<div class="card-content">
<strong><%= @character.name %></strong>
<ul>
<li>
Completed <%= time_ago_in_words commission.completed_at %> ago
</li>
<li>
Took <%= distance_of_time_in_words commission.completed_at - commission.created_at %>
</li>
<hr />
<li>
You can click on the image to download it. Congratulations, it's yours now!
Feel free to upload it to your character's page if you want to keep and/or use it.
</li>
</ul>
</div>
</div>
</div>
</div>
<% else %>
<div class="card-panel green white-text darken-4">
Basil is still working on this commission...
<div style="font-size: 0.8em">
(Requested <%= time_ago_in_words(commission.created_at) %> ago)
<% else %>
<div class="card-panel green white-text darken-4">
Basil is still working on this commission...
<div style="font-size: 0.8em">
(Requested <%= time_ago_in_words(commission.created_at) %> ago)
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<script type="text/javascript">
function commission_basil(style) {
// Set style hidden value to our selected style
$('#basil_commission_style').val(style);
// Submit form to start the commission
$('#new_basil_commission').submit();
}
</script>

View File

@ -15,6 +15,7 @@
<div class="card-stacked">
<div class="card-content">
<div>
<%= commission.id %>.
<strong><%= link_to commission.entity.name, commission.entity %></strong>
<% if commission.style? %>
<em>(<%= commission.style.humanize %>)</em>

View File

@ -5,9 +5,12 @@ Rails.application.routes.draw do
scope :ai, path: '/ai' do
scope :basil do
get '/', to: 'basil#index', as: :basil
get '/character/:id', to: 'basil#character', as: :basil_character
get '/character/:id/commission', to: 'basil#commission_character', as: :basil_commission_character
post '/character/:id', to: 'basil#commission'
#get '/character/:id/commission', to: 'basil#commission_character', as: :basil_commission_character
# TODO this should also be a POST
get '/complete/:jobid', to: 'basil#complete_commission'
get '/info', to: 'basil#info'