From 1d3361c425d0c363674ec4dcb2eeace90d71afca Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Tue, 3 Mar 2026 15:41:24 +0000 Subject: [PATCH] BETA: accurate color extractor --- lib/music_library/colors/k_means_extractor.ex | 17 +++++++++++++++++ lib/music_library/records.ex | 2 +- lib/music_library/worker/extract_colors.ex | 5 ++++- .../live/collection_live/show.ex | 15 +++++++++++++++ mix.exs | 2 ++ mix.lock | 2 ++ priv/gettext/default.pot | 5 +++++ priv/gettext/en/LC_MESSAGES/default.po | 5 +++++ 8 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 lib/music_library/colors/k_means_extractor.ex diff --git a/lib/music_library/colors/k_means_extractor.ex b/lib/music_library/colors/k_means_extractor.ex new file mode 100644 index 00000000..756115cd --- /dev/null +++ b/lib/music_library/colors/k_means_extractor.ex @@ -0,0 +1,17 @@ +defmodule MusicLibrary.Colors.KMeansExtractor do + @moduledoc """ + Extracts dominant colors from images using K-Means clustering via the dominant_colors library. + """ + + alias Vix.Vips.Image + + def extract_dominant_colors(image_data, num_colors \\ 5) do + with {:ok, dir} <- Briefly.create(type: :directory), + path = dir <> "/temp_image.jpg", + {:ok, image} <- Image.new_from_buffer(image_data), + :ok <- Image.write_to_file(image, path), + {:ok, colors} <- DominantColors.dominant_colors(path) do + {:ok, Enum.take(colors, num_colors)} + end + end +end diff --git a/lib/music_library/records.ex b/lib/music_library/records.ex index 7b3e669c..db2b7b9b 100644 --- a/lib/music_library/records.ex +++ b/lib/music_library/records.ex @@ -375,7 +375,7 @@ defmodule MusicLibrary.Records do def create_record(attrs \\ %{}) do with {:ok, record} <- do_create_record(attrs) do - extract_colors_async(record, :fast) + extract_colors_async(record, :accurate) generate_embedding_async(record) record diff --git a/lib/music_library/worker/extract_colors.ex b/lib/music_library/worker/extract_colors.ex index fb6aa4c6..9e86d60e 100644 --- a/lib/music_library/worker/extract_colors.ex +++ b/lib/music_library/worker/extract_colors.ex @@ -2,7 +2,7 @@ defmodule MusicLibrary.Worker.ExtractColors do use Oban.Worker, queue: :heavy_writes, max_attempts: 3 alias MusicLibrary.{Assets, Records} - alias MusicLibrary.Colors.{ColorFrequencyExtractor, EdgeWeightedExtractor} + alias MusicLibrary.Colors.{ColorFrequencyExtractor, EdgeWeightedExtractor, KMeansExtractor} @impl Oban.Worker def perform(%Oban.Job{args: %{"id" => record_id, "method" => method}}) do @@ -21,4 +21,7 @@ defmodule MusicLibrary.Worker.ExtractColors do defp extract_colors(image_data, :slow), do: EdgeWeightedExtractor.extract_dominant_colors(image_data) + + defp extract_colors(image_data, :accurate), + do: KMeansExtractor.extract_dominant_colors(image_data) end diff --git a/lib/music_library_web/live/collection_live/show.ex b/lib/music_library_web/live/collection_live/show.ex index 0199d0ef..5d96dbae 100644 --- a/lib/music_library_web/live/collection_live/show.ex +++ b/lib/music_library_web/live/collection_live/show.ex @@ -195,6 +195,21 @@ defmodule MusicLibraryWeb.CollectionLive.Show do {gettext("Extract colors (slow)")} + <.dropdown_link + id={"actions-#{@record.id}-extract-colors-accurate"} + phx-click={ + JS.push("extract_colors", value: %{id: @record.id, method: :accurate}) + } + > + <.icon + name="hero-paint-brush" + class="h-4 w-4 mr-1 phx-click-loading:animate-shake" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Extract colors (accurate)")} + + <.dropdown_separator /> <.dropdown_link id={"actions-#{@record.id}-delete"} diff --git a/mix.exs b/mix.exs index be019dd1..21cb24ba 100644 --- a/mix.exs +++ b/mix.exs @@ -96,6 +96,8 @@ defmodule MusicLibrary.MixProject do # Image manipulation {:vix, "~> 0.38.0"}, + {:dominant_colors, "~> 0.1.4"}, + {:briefly, "~> 0.5.0"}, # Notes {:earmark, "~> 1.4"}, diff --git a/mix.lock b/mix.lock index 1201015a..f2bdce67 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, + "briefly": {:hex, :briefly, "0.5.1", "ee10d48da7f79ed2aebdc3e536d5f9a0c3e36ff76c0ad0d4254653a152b13a8a", [:mix], [], "hexpm", "bd684aa92ad8b7b4e0d92c31200993c4bc1469fc68cd6d5f15144041bd15cb57"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, @@ -11,6 +12,7 @@ "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, + "dominant_colors": {:hex, :dominant_colors, "0.1.4", "afdc765c5156ad3eb9016827e86061f778c5b1d4d879977317be80f32be375ba", [:mix], [{:rustler, "~> 0.30.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8196358f6491a051e84460c622bcbe01ef1daae9e539e13f9b304e3379f09e8b"}, "earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 92b6b12d..01aca59f 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2063,3 +2063,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Failed to store cover image" msgstr "" + +#: lib/music_library_web/live/collection_live/show.ex +#, elixir-autogen, elixir-format +msgid "Extract colors (accurate)" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 959df129..ea4528de 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2063,3 +2063,8 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "Failed to store cover image" msgstr "" + +#: lib/music_library_web/live/collection_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Extract colors (accurate)" +msgstr ""