diff --git a/app/models/weather_art.rb b/app/models/weather_art.rb
index 1eea6e5..344fe8b 100644
--- a/app/models/weather_art.rb
+++ b/app/models/weather_art.rb
@@ -4,7 +4,6 @@ class WeatherArt < ApplicationRecord
belongs_to :city
has_one_attached :image
- has_one_attached :image_with_watermark
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
@@ -59,4 +58,145 @@ class WeatherArt < ApplicationRecord
def image_url
image.attached? ? image.blob : nil
end
+
+ def webp_image
+ return nil unless image.attached?
+
+ image.variant(
+ format: "webp",
+ saver: {
+ quality: 100,
+ strip: true, # 移除元数据以减小文件大小
+ interlace: "plane" # 渐进式加载
+ }
+ )
+ end
+
+ # 添加图片变体处理
+ PREVIEW_DIMENSIONS = {
+ big: [ 1792, 1024 ],
+ medium: [ 896, 512 ],
+ small: [ 448, 256 ]
+ }.freeze
+ def preview_image(size = :medium)
+ return nil unless image.attached?
+
+ width, height = PREVIEW_DIMENSIONS[size] || PREVIEW_DIMENSIONS[:medium]
+
+ image.variant(
+ resize_to_limit: [ width, height ],
+ format: "webp",
+ saver: {
+ quality: 75,
+ strip: true, # 移除元数据以减小文件大小
+ interlace: "plane" # 渐进式加载
+ }
+ )
+ end
+
+ def watermarked_image
+ return nil unless image.attached?
+
+ overlay_text = create_overlay_text
+
+ image.variant(
+ composite: [ {
+ input: overlay_text,
+ gravity: "southeast"
+ } ]
+ )
+ end
+
+ private
+
+ def create_overlay_text
+ {
+ create: {
+ width: 400,
+ height: 100,
+ background: [ 0, 0, 0, 0.5 ] # 半透明黑色背景
+ },
+ "svg-overlay": %(
+
+ )
+ }
+ end
+
+ def create_text_layer(font_size)
+ text = [
+ weather_date.strftime("%Y-%m-%d"),
+ "#{temperature}°C, #{description}",
+ "#{city.name}, #{city.country.name}, #{city.country.region.name}",
+ "© todayaiweather.com"
+ ].join("\n")
+
+ {
+ create: {
+ width: 600,
+ height: 200,
+ background: [ 0, 0, 0, 0 ]
+ },
+ "svg-overlay": %(
+
+ )
+ }
+ end
+
+ def watermark_command(font_size:, stroke_width:, spacing:)
+ date_str = weather_date.strftime("%Y-%m-%d")
+ weather_info = "#{temperature}°C, #{description}"
+ location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
+ copyright = "© todayaiweather.com"
+
+ "gravity southeast " \
+ "fill white " \
+ "font Arial " \
+ "pointsize #{font_size} " \
+ "stroke black " \
+ "strokewidth #{stroke_width} " \
+ "text 30,#{spacing * 12} '#{copyright}' " \
+ "text 30,#{spacing * 8} '#{location_info}' " \
+ "text 30,#{spacing * 4} '#{weather_info}' " \
+ "text 30,#{spacing} '#{date_str}'"
+ end
+
+ def watermark_text
+ date_str = weather_date.strftime("%Y-%m-%d")
+ weather_info = "#{temperature}°C, #{description}"
+ location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
+ copyright = "© todayaiweather.com"
+
+ [
+ "text 30,120 '#{copyright}'",
+ "text 30,80 '#{location_info}'",
+ "text 30,40 '#{weather_info}'",
+ "text 30,0 '#{date_str}'"
+ ].join(" ")
+ end
end
diff --git a/app/services/ai_service.rb b/app/services/ai_service.rb
index 97a0bd0..dff7a01 100644
--- a/app/services/ai_service.rb
+++ b/app/services/ai_service.rb
@@ -3,6 +3,7 @@ class AiService
@client = OpenAI::Client.new(
access_token: Rails.application.credentials.openai.token,
uri_base: Rails.application.credentials.openai.uri,
+ log_errors: Rails.env.development?, # 只在开发环境下启用
request_timeout: 240
)
end
@@ -47,8 +48,8 @@ class AiService
def ask_ai(system_message, user_message)
response = @client.chat(
parameters: {
- model: "gpt-4",
- message:
+ model: "gpt-4o",
+ messages:
[ {
role: "System",
content: system_message
diff --git a/app/services/weather_service.rb b/app/services/weather_service.rb
index e350891..f77fa4f 100644
--- a/app/services/weather_service.rb
+++ b/app/services/weather_service.rb
@@ -31,8 +31,8 @@ class WeatherService
pressure: data["pressure"].to_f,
visibility: data["vis"].to_f,
cloud: data["cloud"].to_f,
- description: data["text"],
- time: response["updateTime"]
+ description: data["text"]
+ # time: response["updateTime"]
}
end
end
diff --git a/app/views/arts/index.html.erb b/app/views/arts/index.html.erb
index 7c677a9..42a55b1 100644
--- a/app/views/arts/index.html.erb
+++ b/app/views/arts/index.html.erb
@@ -6,7 +6,7 @@
<% if featured_art&.image&.attached? %>
- <%= image_tag featured_art.image,
+ <%= image_tag featured_art.webp_image.processed,
class: "w-full h-full object-cover" %>
@@ -107,7 +107,7 @@
<% if art.image.attached? %>
- <%= image_tag art.image,
+ <%= image_tag art.preview_image.processed,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
diff --git a/app/views/cities/_city.html.erb b/app/views/cities/_city.html.erb
index bf0ba53..438d472 100644
--- a/app/views/cities/_city.html.erb
+++ b/app/views/cities/_city.html.erb
@@ -5,7 +5,7 @@
- <%= image_tag city.latest_weather_art.image,
+ <%= image_tag city.latest_weather_art.preview_image.processed,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
diff --git a/app/views/cities/index.html.erb b/app/views/cities/index.html.erb
index 07782c9..425138f 100644
--- a/app/views/cities/index.html.erb
+++ b/app/views/cities/index.html.erb
@@ -6,7 +6,7 @@
<% if featured_art&.image&.attached? %>
- <%= image_tag featured_art.image,
+ <%= image_tag featured_art.webp_image.processed,
class: "w-full h-full object-cover object-center" %>
diff --git a/app/views/cities/show.html.erb b/app/views/cities/show.html.erb
index 654b980..b0d504b 100644
--- a/app/views/cities/show.html.erb
+++ b/app/views/cities/show.html.erb
@@ -2,7 +2,7 @@
<% if @city.latest_weather_art&.image&.attached? %>
- <%= image_tag @city.latest_weather_art.image,
+ <%= image_tag @city.latest_weather_art.webp_image.processed,
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
@@ -103,7 +103,7 @@
<% if art.image.attached? %>
- <%= image_tag art.image,
+ <%= image_tag art.preview_image.processed,
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
diff --git a/app/views/weather_arts/show.html.erb b/app/views/weather_arts/show.html.erb
index b3261df..162fcdc 100644
--- a/app/views/weather_arts/show.html.erb
+++ b/app/views/weather_arts/show.html.erb
@@ -27,15 +27,15 @@
- <% blob = @weather_art.image_blob %>
- <%= link_to rails_blob_path(blob),
+ <% watermarked = @weather_art.webp_image.processed %>
+ <%= link_to rails_blob_path(watermarked),
data: {
- pswp_src: rails_blob_url(blob),
+ pswp_src: rails_blob_url(watermarked),
pswp_caption: 'Weather Art',
pswp_width: 1792,
pswp_height: 1024
} do %>
- <%= image_tag @weather_art.image, class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
+ <%= image_tag @weather_art.preview_image(:big).processed, class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
<%#= image_tag @weather_art.watermarked_variant.processed , class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
<% end %>
diff --git a/app/workers/batch_generate_weather_arts_worker.rb b/app/workers/batch_generate_weather_arts_worker.rb
index a7deda0..2d29c0e 100644
--- a/app/workers/batch_generate_weather_arts_worker.rb
+++ b/app/workers/batch_generate_weather_arts_worker.rb
@@ -1,38 +1,85 @@
class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker
+
GENERATION_INTERVAL = 36.hours
MAX_DURATION = 50.minutes
SLEEP_DURATION = 120.seconds
- BATCH_SIZE = 20
-
- module ProcessType
- DO_NOTHING = 0
- GET_WEATHER_WITH_GENERATE_AI_ART = 1
- GET_WEATHER_ONLY = 2
- end
+ DAILY_GENERATION_LIMIT = 50 # 每日生成图片上限
def perform(*args)
start_time = Time.current
- cities_to_process = get_eligible_cities.shuffle.take(BATCH_SIZE)
- cities_to_process.each do |city|
+ remaining_slots = calculate_remaining_slots
+ return if remaining_slots <= 0
+
+ # 获取符合条件的城市并处理
+ cities_to_process = select_cities(remaining_slots)&.shuffle
+ print_cities_list(cities_to_process, start_time)
+ process_cities(cities_to_process, start_time)
+ end
+
+ private
+
+ def calculate_remaining_slots
+ today_generations = WeatherArt
+ .where("DATE(created_at) = ?", Date.today)
+ .where.not(image_attachment: nil)
+ .count
+
+ [ DAILY_GENERATION_LIMIT - today_generations, 0 ].max
+ end
+
+ def print_cities_list(cities, start_time)
+ Rails.logger.info "Generate city task start at: #{start_time}"
+ Rails.logger.info "Generate city list: "
+ Rails.logger.info "======================================"
+ Rails.logger.info "ID\tRegion\tCountry\tState\tName"
+ cities.each do |city|
+ Rails.logger.info "#{city.id}\t#{city&.country&.region&.name}\t#{city&.country&.name}\t#{city&.state&.name}\t#{city&.name}"
+ end
+ end
+
+ def select_cities(limit)
+ cutoff_time = Time.current - GENERATION_INTERVAL
+
+ # 基础查询:排除最近生成过的城市
+ base_query = City
+ .joins("LEFT JOIN (
+ SELECT city_id, MAX(created_at) as last_generation_time
+ FROM weather_arts
+ GROUP BY city_id
+ ) latest_arts ON cities.id = latest_arts.city_id")
+ .where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
+
+ # 优先选择活跃城市
+ active_cities = base_query
+ .where(active: true)
+ .order(:priority)
+ .limit(limit)
+ .to_a
+
+ remaining_slots = limit - active_cities.size
+
+ if remaining_slots > 0
+ # 如果还有剩余名额,从其他城市中随机选择
+ other_cities = base_query
+ .where.not(id: active_cities.map(&:id))
+ .order("RANDOM()")
+ .limit(remaining_slots)
+ .to_a
+
+ active_cities + other_cities
+ else
+ active_cities
+ end
+ end
+
+ def process_cities(cities, start_time)
+ cities.each do |city|
break if Time.current - start_time > MAX_DURATION
+
Rails.logger.info "Generating weather art for #{city.name}"
GenerateWeatherArtWorker.perform_async(city.id)
sleep SLEEP_DURATION
end
end
-
- private
-
- def get_eligible_cities
- cutoff_time = Time.current - GENERATION_INTERVAL
- City.active
- .joins("LEFT JOIN (
- SELECT city_id, MAX(created_at) as last_generation_time
- FROM weather_arts
- GROUP BY city_id
- ) latest_arts ON cities.id = latest_arts.city_id")
- .where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
- .order(:priority)
- end
end
diff --git a/app/workers/generate_weather_art_worker.rb b/app/workers/generate_weather_art_worker.rb
index 0412798..44ec8d5 100644
--- a/app/workers/generate_weather_art_worker.rb
+++ b/app/workers/generate_weather_art_worker.rb
@@ -14,6 +14,7 @@ class GenerateWeatherArtWorker
return unless image_url
create_weather_art(weather_data, prompt, image_url)
+ Rails.logger.info "Successful Generate Weather Art In #{city.name}"
rescue StandardError => e
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc
index 75cc769..7f598a1 100644
--- a/config/credentials.yml.enc
+++ b/config/credentials.yml.enc
@@ -1 +1 @@
-WIRCFJoA1YUpdSuljSkBqEJvBpftvTRGA73T9Oy9vXNnOIHcnZDHtlhh2VuF4Xtq41d0aPJ2qPI4kUeGzM34I6FITsSY6i1y5ZEW4vUa8F/BRTO0/viXBPiSAS+BkWVEWYrFkNWJAIitVLG0hq8vCBEEs/KctxY8w9h0LwgyRV2udPzn6A7wyoJhR/V5nRamU6USFiHh6cpTlFQdSIZ0sC6v21IIS+G/ZPJXNNTw2+gVaDfL1yuX1nrLsxeMsb30iugksDj/gCVi42kF/hPVOGIZCM1ftM5j4Q+BcTDyEVqIMeXRdtegLKWM5sI0yB070m7gMRK1Oewa3z+NuPLGp1Stuuo977LGuATmP3/GnAMEZe8tgMfeKYeQv2+TerpLmO07KayOzN3j0qSs/OqrcSUP+SByxRBmpmeXNEQ6q+f1ZDreo/Q1Cm+aZe1UWXYpcgd6MVGHjYUna4SgTQqUKHKdzvf7Yx8fOjrzHRqZ6Y8LWq53Vzr8oNJ9IoNAh9TSJMF4tR3M+OQ7+SARgwQLoVsehgK1Z658GMvhVoeLrPTwvdID54+TSFMepMnownDJPZGKZoZK1+NlUyzz3rJ2iH9AxZnvwPMCcmlHH/Fh/FANmtQxd3DIPpjHHyZjxFLsR9tNuIapXPK8SiRhWbtYo+4i0/dgUvD8SfECSOklfx2tVIhvxTjg4AM+WtI9gieDISq+AZe9fjqyv8vLOJuDX8Nk1k69DKIqB2u+OFJW2/PPYFz2Ry283ep+VRKwS2qlsFEkTRkzPJ5y9dVrCXIYl0R7vgC+GDrUkK1IffZmTaJ+rqmzzwLF1OwPdtN/QuJm0aZZQmN9pQgUjt1PEgthQVW9dt/ElDZKgVjIcds3F1kX0+ZWJQRvBldilC9W5lRSea/9x253dw3wj1ERF6sZsIJCwcxTJigTM4rEfcjrMnEIfiW1HWc1LTJ5DUIh5GISBv7NDF52voP2TBamAq2Zg2vVg2v+bU9ydkkIVOm4oLYZWDQyDBeQC4KJscFCSo1ZxvRuqIRtnI3h2KoXJeK63cxGcVxbHcyYO5xTPXmPkk18pFPYoXe46an6xTEzxYXQMfWuIUvM9oOjgZXFvdgPwDWzL/pSZb6qW5MiDqvlt38ddivUtsZLMMtpgZjXOAyKQyIQNSOHyrrYoj6r8PdFiCZMWjn1B0HmXgIl47jrb9o1nrif9rb5DSUY2Pvl0+TeRnUB8r7Wa+t+UdHjrq6PEaXVOQbej78Rzo5xmZ5XknlbKv02Vjpcm8EIkc00nKvMo29f0mr1Wv48CL+tuDxiW3v2yX8NcTnMEoQkTTaw5wJmSb3dmPdVZwjcCdbqs1CbRpc/ngX7nJP9kF2P57BWqqYq6V4fWk3vEgLpTUsTCbJvTlCx7McLKy2/smtz2HQl6Vs5jofpSIXgNC35TBkPP22XLFiBTah0E2IUeF2TyuLphswQsMDzKzADMxkKpzEVNLCzzKfygDfsfsJqZjKBGHJz3RHDbGoqmgP/4u7FEBV9gUXFt5i5k71HxSnafbpcFkLk3AdfNLso2qUNZSj/8EzY33WFjQIEAFJeTqB8WML/LcVvNqQqvjUyUMgvA+rZq+BwS8svqJEvcRFmZhjeqQbsYs+C+jOlwjrohoak/CgfiHofuXWvnNyi0+PT6bPO5CsjbpJLwFxbPi19tWmnMR7ZOqIE3vN4bf8Rjg4T5zhKKbgIU7wEP+i0u9p0zPFEa6EZMxp8/5q50EjXdlY/cHa/Sp05x2h8jhi1Trxgg8yFOdu2P+dStAwW3Sv4x2o=--aZKHys4Q3dCV2P+L--yEOdlrHAYleGIYmdKAB4FQ==
\ No newline at end of file
+mNiwLK27M1vAWefYLzYIiQO0jBJqqZ1Yo0T1YOcldA3YrQo+AVm1vImv7Nbc96N3OK4f782Q3lNp4mTPl0zUFjiWy2QN/VdBMN9B2uxcUXJIASCJMwDlrc+e2evD6dqUb/hTj0q2Hi/uMVuRBI1O/bDvxxBQma/4MyPgwm6uwnP2g1yLfiBkgveU72yc4tHHmzXwEoxtJa3VkVKus+QWwCyWe/ZKvoDF2RplQm7RYVXgsbNhSdTVfoGvH9ExKRZvKjzrXutLtXe4YpHiiLSisCQBLYqF/XptG86U/uFldtCcbcB6XX2psyrQxHmhdxHnucMSx35rvh4XrjPMNKDpCiYuvJepjaJzgPDRkxjf4txIUHnIs3p6lVto+b92vq2OGKAq/nUwTpAB46WVz4CNCI0Z+QHkbSZ05a6cHAkzBY1Z4uiwu7gM6AFN/jYOdDo6GzxrP6h/Msp3wmOgKiM8Ts1kysXNdJ6E8X8GHPNKfHLnyVnoVrJbY1GGv6oXu4WkIxAK5v6Mi10J0Sd6QQJuZYv2SKNCjXUtxrm4NFWd9DY3BMvF3TSDRU17riruPyJHCYKSqCQHobblC7Sr1lwcN3+CfOJqGXR66D0OH6DNiHwobxsyVSHzSePl5bVi9a44V+GtzDJ0fZQYOpVnzWOsMj++K89dz2qtruB7BSOQe9YAXGk+dk//XqUebz4fqYUiW79aj4M1DyFquapbcn+WpzmXFGuVORM2ZuQjUr0Glev8JMq95R4JIZkMDwm5/q7YUbqEb25yMdhs5c48vmCIy3itliieWN9kYZO6pqIsP73KXms2Xi5nJ+FcaFQ9T24zs/S9JcA1a2KUl6n8kYBcVYBdNBvE+9ab+eJ3RUp3YMOErmMHAtTsUXQCZLmXpRheaaOkupS/XB+M+jYgYLI6+sv86ISZsYz472iT9UMCn5F/X8lNIwDClPoJP2kafLDq6hMlo76Oa/aVj+miy7tJIWFgVvMJ3VwPf526MMZgNShF6tZ+Z7PA0pr+9WuoZSyQ0Ai9lql/Tk+r2ZUPT6Z28qjGr9ln2jbb509ETwhSxnHjINskNyLmfFWUcELy+BEmOSn9GQgay0PoFkUrYpidpXD9Tw2IpEpklyIGhr4HFHv4bQ5uBWjPrIGm3R8fjATA0xB+Wg9SDPSHd+QclAzcL6Po8UlLIZfROjReg7ZJn5+8x0rf7UidNXTfy+JM0GmUR9aUh110RKE0u1WjdhhzdVDiQH2m58CZf3TsP3z4o8SLWfXAftqU8p3sBXPOV+L5RyWrUMzvWmDFzL0RmP854fOILsq76QoI4PULPHD7AIGaCHxuM+qmNQvYhFBMDCaHEeLP4ARX2NmUxMCT40T7bWEFbrAJ14Kd8L/EAjYVsGrBTWGZSvB3aZcZnP3cy4kC9XWvfAksstm9dPSqbxH7d8POb5KJBhSnfm5UWczc24s/6TC9UON921Shz3e9tk7bbBSBNIA/pHWmoZ8vFS3Cegihnt9TnALlWDBrGm7arYr3oQJHqkQ4KPEh36EwZDw/+YDaXLhN2yR8hL7nGPsewcwKdYEU4bGjTpI6xtjs0hBgwuMwTNWUsEm+bijt2feTxzJgUyT9ywW7aSsDfZi88J06LMJfpeAUIjhTjB9Coohk33lrpmsrD5Hb3RaObELlrpG6RkCHlmTxMTjTonS9ni+7dXUYJFYYvtLuyFmhSxEEk7bCrCMVkOWN5tTs0y0J22bxT/FrGLn/GKKlK/FVAeoGN8ZvEOCmIizEQ8k6wgAbuLDaTxV1irLLOeaY3JtjwVxBQHfNuCgxKXxd0zcUqv7D/sgMC0zafcqshG8VaXfGvFexWVEnZjPiZ6aHzeNMhdTPsf9D0p8R3PFgcvC2rSLYNbj2MXnvGqkrtQCre0CiNVxflVvOHbJjugdp5LqAjf6bssCsrZKifaRTPeg=--B6BEVJN27qT5bDUL--B4LzjOJ7SlzEKibq4VSVmg==
\ No newline at end of file
diff --git a/config/sidekiq_scheduler.yml b/config/sidekiq_scheduler.yml
index 55bbeee..6963062 100644
--- a/config/sidekiq_scheduler.yml
+++ b/config/sidekiq_scheduler.yml
@@ -1,7 +1,7 @@
batch_generate_weather:
cron: '0 8,18 * * *'
class: BatchGenerateWeatherArtsWorker
- description: "Generate weather arts every 2 hours"
+ description: "Batch Generate weather arts"
enabled: true
refresh_sitemap: