Skip to content

Commit

Permalink
Improve album art (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcraigk authored Oct 6, 2024
1 parent 1ac5be9 commit d68cf6b
Show file tree
Hide file tree
Showing 15 changed files with 1,276 additions and 1,275 deletions.
11 changes: 6 additions & 5 deletions app/javascript/components/CoverArt.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ const CoverArt = ({ coverArtUrls, albumCoverUrl, openAppModal, size = "small", c
if (!openAppModal) return;
const modalContent = (
<>
{albumCoverUrl && (
<div className="large-album-art">
<img src={albumCoverUrl} alt="Album cover" />
</div>
)}
{coverArtUrls?.large && (
<div className="large-album-art mt-3">
<img src={coverArtUrls?.large} alt="Cover art" />
</div>
)}

{albumCoverUrl && (
<div className="large-album-art">
<img src={albumCoverUrl} alt="Album cover" />
</div>
)}

{prompt && (
<div className="box prompt mt-2">
<p>
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/components/CoverArtInspector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const coverArtInspectorLoader = async ({ request }) => {
}
};

import React, { useState } from "react";
import React from "react";
import { useLoaderData, useNavigate, useOutletContext } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import LayoutWrapper from "./layout/LayoutWrapper";
Expand All @@ -41,7 +41,7 @@ const CoverArtInspector = () => {

const sidebarContent = (
<div className="sidebar-content">
<p className="sidebar-title">Album Covers</p>
<p className="sidebar-title">Cover Art</p>
<p className="sidebar-subtitle">{totalEntries} total</p>
</div>
);
Expand All @@ -61,6 +61,7 @@ const CoverArtInspector = () => {
openAppModal={openAppModal}
size="medium"
css="cover-art-inspector"
prompt={show.cover_art_prompt}
/>
))}
</div>
Expand Down
5 changes: 2 additions & 3 deletions app/javascript/components/controls/Player.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,14 @@ const Player = ({ activePlaylist, activeTrack, setActiveTrack, audioRef, customP
}

if ('mediaSession' in navigator) {
let imgSrc = 'https://phish.in/static/logo-square-512.png'
navigator.mediaSession.metadata = new MediaMetadata({
title: activeTrack.title,
artist: `Phish - ${formatDate(activeTrack.show_date)}`,
album: `${formatDate(activeTrack.show_date)} - ${activeTrack.venue_name}`,
artwork: [
{
src: activeTrack.show_cover_art_urls.medium || imgSrc,
sizes: '512x512',
src: activeTrack.show_album_cover_url,
sizes: '1024x1024',
type: 'image/png',
}
]
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/stylesheets/content.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
.cover-art-small {
width: 32px;
height: 32px;
filter: grayscale(0.5);
filter: grayscale(1);
top: 2px;
}
}
Expand Down
6 changes: 3 additions & 3 deletions app/javascript/stylesheets/modal.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
background: white;
padding: 1.5rem;
border-radius: var(--bulma-radius);
margin: 3rem auto auto auto;
max-height: 600px;
margin: 1rem auto auto auto;
max-height: 800px;
max-width: 1200px;
overflow: auto;

Expand Down Expand Up @@ -41,7 +41,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
z-index: 103;
}

@media (min-width: 1023px) {
Expand Down
8 changes: 4 additions & 4 deletions app/jobs/album_zip_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ def create_and_attach_album_zip
end

# taper-notes.txt
zipfile.get_output_stream("taper-notes.txt") do |stream|
stream.write show.taper_notes
zipfile.get_output_stream("taper_notes.txt") do |stream|
stream.write "#{show.taper_notes}\n\n=== Downloaded from https://phish.in ==="
end

# cover-art.jpg
if show.cover_art.attached? && show.cover_art
zipfile.get_output_stream("cover-art.jpg") do |stream|
zipfile.get_output_stream("cover_art.jpg") do |stream|
stream.write show.cover_art.download
end
end

# album-cover.jpg
if show.album_cover.attached?
zipfile.get_output_stream("album-cover.jpg") do |stream|
zipfile.get_output_stream("album_cover.jpg") do |stream|
stream.write show.album_cover.download
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/has_cover_art.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def generate_album!
CoverArtImageService.call(self)
AlbumCoverService.call(self)
tracks.each(&:apply_id3_tags)
AlbumZipJob.perform_async(id)
# AlbumZipJob.perform_async(id)
end

def album_zip_url
Expand Down
21 changes: 14 additions & 7 deletions app/services/album_cover_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def composite_text_on_cover_art
# Bg dropshadow
gradient_path = Rails.root.join("tmp", "#{SecureRandom.hex}.png").to_s
MiniMagick::Tool::Convert.new do |cmd|
cmd.size "#{@art.width}x#{(@art.height * 0.03).to_i}"
cmd.gradient "none-rgba(34,34,34,0.8)"
cmd.size "#{@art.width}x#{(@art.height * 0.015).to_i}"
cmd.gradient "none-rgba(34,34,34,0.55)"
cmd << gradient_path
end

Expand Down Expand Up @@ -67,22 +67,22 @@ def composite_text_on_cover_art
end

# Date
text = show.date.to_s.gsub("-", ".")
text = show.date.strftime("%b %-d, %Y")
@art.combine_options do |c|
c.gravity "SouthEast"
c.font font2
c.pointsize 65
c.pointsize 60
c.antialias
c.fill text_color
c.draw "text 40,80 '#{text}'"
c.draw "text 40,92 '#{text}'"
end

# Venue
text = show.venue_name.truncate(50, omission: "...").gsub("'", "\\\\'")
text = smart_truncate(show.venue_name).gsub("'", "\\\\'")
@art.combine_options do |c|
c.gravity "SouthEast"
c.font font2
c.pointsize 32
c.pointsize 40
c.antialias
c.fill text_color
c.draw "text 40,42 '#{text}'"
Expand All @@ -92,6 +92,13 @@ def composite_text_on_cover_art
File.delete(gradient_path) if File.exist?(gradient_path)
end

# Remove any non-alphabetic characters before the omission
def smart_truncate(text, length: 35, omission: "...")
return text if text.length <= length
text[0...(length - omission.length)].sub(/[^a-zA-Z]+$/, "") + omission
end


def attach_album_cover
album_cover_path = Rails.root.join("tmp", "#{SecureRandom.hex}.jpg")
@art.write(album_cover_path.to_s) { |img| img.quality 90 }
Expand Down
83 changes: 40 additions & 43 deletions app/services/cover_art_prompt_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,52 @@ class CoverArtPromptService < BaseService
HUES = %w[
Red Orange Yellow Green Blue Purple
Red-Orange Yellow-Orange Yellow-Green Blue-Green
Blue-Violet Red-Violet Pink Magenta Vermilion
Pink Magenta Vermilion
Scarlet Cyan Teal Turquoise Indigo Lavender
Amethyst Chartreuse Lime Crimson Maroon Olive
Burgundy Ochre Beige Peach Mint Navy Coral
Rose Amber Emerald Sapphire Violet Periwinkle
Fuchsia Aquamarine Mint\ Green Apricot Mustard
Fuchsia Aquamarine Mint Apricot Mustard
Tangerine Plum
]
STYLES = %w[
Abstract Impressionism Cubism Surrealism Minimalism Expressionism
Pop-Art Art-Deco Art-Nouveau Futurism Dadaism Bauhaus
Gothic Baroque Romanticism Renaissance Neo-Expressionism
Pointillism Fauvism Graffiti Collage Ink-Drawing
Watercolor Line-Art Cartoon Geometric Psychedelic
Pixel-Art Low-Poly Vaporwave Retro Vintage
Ukiyo-e Pastel Stained-Glass Mosaics
Woodcut Block-Prints Cyberpunk Steampunk
Abstract Impressionism Minimalism Expressionism
Pop-Art Art-Deco Art-Nouveau Futurism Chinese-Brush-Painting
Gothic Wood-Burned Technical-Drawing
Pointillism Fauvism Ink-Drawing Illustration
Watercolor Line-Art Geometric Charcoal
Low-Poly Pencil-Drawing Oil-Painting
Pastel Stained-Glass Mosaics Childrens-Book
Woodcut Block-Prints Comic-Book
]
BASE_PROMPT = <<~TXT
We are going to generate an optimized prompt for DALL-E to create an artistic square image based on style, hue, and a few subjects pulled from a live musical performance held in the real world. I will provide general information of the event including time, place, and setlist. I will also provide data for a previous event to avoid repetition of imagery. The format of your response will take this JSON format. Always respond with pure JSON and always include "subjects" and "prompt" keys. Here is an example:
We are going to generate an optimized prompt for DALL-E to create an artistic square image based on style, hue, and a few subjects pulled from a live musical performance.
{
"subjects": "Skyscrapers, llamas, and a UFO",
"prompt": "Create a rock album cover that includes images of skyscrapers, llamas, and a ufo in an abstract style with a blue hue",
}
First, here is a **critical set of exclusions** that should never be included under any circumstances:
- No jellyfish, horses, lighthouses, phoenixes, carousels or spinning tops.
- Avoid instruments (guitars, saxophones, brass, classical instruments). No saxophones ever.
- Avoid images of humans, human forms, or faces.
- Avoid clocks, hourglasses, historical figures, and any text or symbols.
- Avoid owls, foxes, flamingos, lobseters, raccoons, cats, lions, dolphins, waves, skeletons, dragons, unicorns, buffalo, tornadoes, cowboy hats, pyramids, violins, bats, bears, ferris wheels, churches, cathedrals, butterflies, pumpkins, gargoyles, and ghosts.
- Avoid cliche landmarks like the Statue of Liberty, Golden Gate Bridge, Liberty Bell, Eiffel tower, and anything related to obvious geography (cornfields, cheese, moose, etc.).
- Avoid swirls, vortexes, kaleidoscope, spirals, fractals, galaxies, meteors, - Avoid kites and and staircases.
- Avoid references that DALL-E might reject as inappropriate.
- Do not mention musical performances or the band Phish. Dall-e should not be aware of the context of the prompt.
Now, let's generate the prompt:
The subjects should be a comma separated list of 3 concepts or images pulled from the event info. Take liberty here and be creative in how these subjects are selected. Consider the time and location of the concert. If a setlist is provided, include the song titles and your knowledge of song lyrical content in consideration. Favor popular songs over obscure ones. Consider cultural and artistic impressions of the songs played at the show to add to the pool of potential imagery. If there appear to be themes in the song selections, favor that. Lean into famous landmarks and famous songs/lyrics and other imagery. Don't be afraid to get creative and silly in some cases. If the show is not in the united states, lean into cultural/geographic references for the foreign country. If the show takes place in a unique location, lean into that uniqueness. For shows that take place on halloween, take note of the cover songs played and lean into that. Season and weather should also be considered.
You will take **style**, **hue**, and **subjects** related to the performance and generate a creative prompt that avoids the aforementioned exclusions. Be creative with subjects, drawing from song titles and themes, but without emphasizing location too heavily. Often the subjects should be more abstract, inspired by themes in the setlist rather than the time and place. The subjects should be a random selection of 2 or 3 concepts pulled from the performance, but never include any of the excluded items listed above.
Do not let Dall-e know that the art is related to a concert, but rather be general about prompting it to generate an art piece based on the selected subjects. Do not let Dall-e generate text or symbols.
Your JSON output should be in this format:
Use your best knowledge about Dall-e to generate an optimized prompt and provide the full text of the prompt in the 'prompt' key of your JSON response. Be sure this prompt includes indication of style, hue, subjects, and any other relevant information for a visually pleasing and unique piece of art.
{
"subjects": "Skyscrapers, llamas, and a UFO",
"prompt": "Create a rock album cover that includes images of skyscrapers, llamas, and a UFO in an abstract style with a blue hue."
}
Your prompt should begin with "Create an image in x style with y hue." and then get creative with the subjects but do not specify any other styles or hues/colors beyond the initial specification.
Never mention any of the excluded items in the prompt. If necessary, create variations, but always respect the exclusions list.
Always respond with pure JSON. Do not include any markdown formatting, such as backticks or newlines outside of JSON structure.
Always respond with pure JSON as specified above.
TXT

def call
Expand All @@ -57,11 +68,14 @@ def generate_new_prompt
cover_art_hue: hue,
cover_art_prompt: chatgpt_response[:prompt],
cover_art_parent_show_id: nil
# puts chatgpt_response[:prompt]
end

def defer_to_kickoff_show
show.update!(cover_art_parent_show_id: run_kickoff_show.id)
show.update! \
cover_art_parent_show_id: run_kickoff_show.id,
cover_art_style: run_kickoff_show.cover_art_style,
cover_art_hue: run_kickoff_show.cover_art_hue,
cover_art_prompt: run_kickoff_show.cover_art_prompt
end

def run_kickoff_show
Expand All @@ -83,42 +97,25 @@ def run_kickoff_show
kickoff_show
end

# Select a hue from our list, selecting a less used one
# and avoiding repetition of the previous show's hue
# Select a hue from our list, voiding repetition of the previous show's hue
def hue
return @hue if defined?(@hue)

available_hues = HUES.dup
if prior_show&.cover_art_hue.present?
available_hues.delete(prior_show.cover_art_hue)
end

hue_usage = Show.where.not(cover_art_hue: nil).group(:cover_art_hue).count
sorted_hues = available_hues.sort_by { hue_usage[_1] || 0 }
min_usage = hue_usage[sorted_hues.first] || 0
bottom_tier_hues = sorted_hues.select { hue_usage[_1] == min_usage || hue_usage[_1].nil? }

@hue = bottom_tier_hues.sample
@hue = available_hues.sample
end


# Select a style from our list, selecting a less used one
# and avoiding repetition of the previous show's style
# Select a style from our list, avoiding repetition of the previous show's style
def style
return @style if defined?(@style)

available_styles = STYLES.dup
if prior_show&.cover_art_style.present?
available_styles.delete(prior_show.cover_art_style)
end

style_usage = Show.where.not(cover_art_style: nil).group(:cover_art_style).count
sorted_styles = available_styles.sort_by { style_usage[_1] || 0 }
min_usage = style_usage[sorted_styles.first] || 0
bottom_tier_styles = sorted_styles.select {
style_usage[_1] == min_usage || style_usage[_1].nil? }

@style = bottom_tier_styles.sample
@style = available_styles.sample
end

# Fetch the previous show not at same venue to avoid duplication
Expand Down
9 changes: 6 additions & 3 deletions app/services/meta_tag_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,17 @@ def resource_name(klass)
def playlist_data
return { title: "Playlists#{TITLE_SUFFIX}", og: {}, status: :not_found } if slug.nil?

playlist = Playlist.includes(:tracks).find_by(slug:)
playlist = Playlist.includes(:tracks, :shows).find_by(slug:)
return { title: "404 - Phish.in", og: {}, status: :not_found } if playlist.nil?

track = playlist.tracks.order(:position).first
{
title: "Listen to #{playlist.name}#{TITLE_SUFFIX}",
og: {
title: "Listen to #{playlist.name}",
type: "music.playlist",
audio: playlist.tracks.order(:position).first&.mp3_url
audio: track&.mp3_url,
image: track&.show&.album_cover_url
},
status: :ok
}
Expand Down Expand Up @@ -128,7 +130,8 @@ def show_data
og: {
title: og_title,
type: "music.playlist",
audio: show.tracks.order(:position).first&.mp3_url
audio: show.tracks.order(:position).first&.mp3_url,
image: show&.album_cover_url
},
status: :ok
}
Expand Down
30 changes: 29 additions & 1 deletion app/services/show_importer/orchestrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def save
show.reload.save_duration
show.update!(published: true)

show.generate_album!
generate_album_interactively

create_announcement

Expand Down Expand Up @@ -98,6 +98,34 @@ def delete(position)

private

def generate_album_interactively
# Step 1: Generate and confirm cover art prompt
loop do
puts "Generating cover art prompt..."
CoverArtPromptService.call(show)
puts "💬 #{show.cover_art_prompt}"
print "(C)onfirm or (R)edo? "
input = $stdin.gets.chomp.downcase
break if input == "c"
end

# Step 2: Generate and confirm cover art image
loop do
puts "Generating cover art..."
CoverArtImageService.call(show)
puts "🏞 #{App.base_url}/blob/#{show.cover_art.blob.key}"
print "(C)onfirm or (R)edo? "
input = $stdin.gets.chomp.downcase
break if input == "c"
end

puts "Making album..."
AlbumCoverService.call(show)
puts "🌌 #{show.album_cover_url}"
show.tracks.each(&:apply_id3_tags)
# AlbumZipJob.perform_async(show.id)
end

def save_song_gaps(show)
GapService.call(show)
GapService.call \
Expand Down
Loading

0 comments on commit d68cf6b

Please sign in to comment.