diff --git a/config/config.exs b/config/config.exs index 64bbbe6..856270f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,6 +2,8 @@ import Config config :plantid_discord_bot, image_path: "priv/static", - plantnet_api_base_url: "https://my-api.plantnet.org/v2" + plantnet_api_base_url: "https://my-api.plantnet.org/v2", + score_threshold: 0.3, + max_results: 5 import_config "#{config_env()}.exs" diff --git a/lib/plantid_discord_bot/cogs/plantnet.ex b/lib/plantid_discord_bot/cogs/plantnet.ex index d13eaaf..4ad7259 100644 --- a/lib/plantid_discord_bot/cogs/plantnet.ex +++ b/lib/plantid_discord_bot/cogs/plantnet.ex @@ -3,15 +3,16 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do alias PlantIdDiscordBot.RateLimiter alias PlantIdDiscordBot.PlantNet.Parser alias PlantIdDiscordBot.FileServer.File + alias Nostrum.Api @api Application.compile_env(:plantid_discord_bot, :api) @plantnet_api_base_url Application.compile_env(:plantid_discord_bot, :plantnet_api_base_url) - - # TEMP: Mock data - @plantnet_raw_response "{\"query\":{\"project\":\"all\",\"images\":[\"https://upload.wikimedia.org/wikipedia/commons/f/ff/Prunus_cerasifera_A.jpg\",\"https://le-jardin-de-pascal.com/2195113-large_default/prunus-cerasifera-atropurpurea-prunier-myrobolan-nigra.jpg\"],\"organs\":[\"auto\",\"auto\"],\"includeRelatedImages\":false,\"noReject\":false},\"language\":\"en\",\"preferedReferential\":\"k-world-flora\",\"bestMatch\":\"Prunus cerasifera Ehrh.\",\"results\":[{\"score\":0.87871,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus cerasifera\",\"scientificNameAuthorship\":\"Ehrh.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Cherry plum, myrobalan\",\"Cherry Plum\",\"Purple-leaf Plum\"],\"scientificName\":\"Prunus cerasifera Ehrh.\"},\"gbif\":{\"id\":\"3021730\"},\"powo\":{\"id\":\"729568-1\"},\"iucn\":{\"id\":\"172162\",\"category\":\"DD\"}},{\"score\":0.31668,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus × cistena\",\"scientificNameAuthorship\":\"N.E.Hansen ex Koehne\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Dwarf red-leaf plum\",\"Purple-leaf sand cherry\",\"Purple-leaved sand cherry\"],\"scientificName\":\"Prunus × cistena N.E.Hansen ex Koehne\"},\"gbif\":{\"id\":\"3022465\"},\"powo\":{\"id\":\"2959315-4\"}},{\"score\":0.01801,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus sargentii\",\"scientificNameAuthorship\":\"Rehder\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Sargent's cherry\",\"Northern Japanese hill cherry\",\"Sargent’s cherry\"],\"scientificName\":\"Prunus sargentii Rehder\"},\"gbif\":{\"id\":\"3020955\"},\"powo\":{\"id\":\"730239-1\"},\"iucn\":{\"id\":\"64127603\",\"category\":\"LC\"}},{\"score\":0.00896,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus × yedoensis\",\"scientificNameAuthorship\":\"Matsum.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Yoshino cherry\",\"Hybrid cherry\",\"Korean flowering cherry\"],\"scientificName\":\"Prunus × yedoensis Matsum.\"},\"gbif\":{\"id\":\"3021335\"},\"powo\":{\"id\":\"30119904-2\"}},{\"score\":0.00518,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus serrulata\",\"scientificNameAuthorship\":\"Lindl.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Japanese flowering cherry\",\"Japanese flowering cherry Kwanzan\",\"Tibetan Cherry\"],\"scientificName\":\"Prunus serrulata Lindl.\"},\"gbif\":{\"id\":\"3022609\"},\"powo\":{\"id\":\"730268-1\"},\"iucn\":{\"id\":\"217170511\",\"category\":\"LC\"}}],\"version\":\"2024-11-19 (7.3)\",\"remainingIdentificationRequests\":488}" + @plantnet_api_key Application.compile_env(:plantid_discord_bot, :plantnet_api_key) + @score_threshold Application.compile_env(:plantid_discord_bot, :score_threshold) + @max_results Application.compile_env(:plantid_discord_bot, :max_results) @doc """ - ID a plant from up to 5 images of organs. + Process /id application command. """ def id(interaction) do case RateLimiter.check_limit(interaction.guild_id) do @@ -30,41 +31,75 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do end defp do_identification(interaction) do + @api.create_interaction_response(interaction, %{ + type: 5, + data: %{ + content: "Processing..." + } + }) + attachment_urls = get_attachment_urls(interaction) original_images = get_original_images(attachment_urls) - # TODO return image filename list as a list of images to delete + # FUNCTION return image filename list as a list of images to delete saved_images = try do File.download_and_save_files!(attachment_urls) rescue e -> - @api.create_interaction_response(interaction, %{ - type: 4, - data: %{ - content: e.message - } + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: e.message }) end IO.inspect(saved_images) - # TODO call PlantNet API and get response, with error handling + # TODO use actual image data + image1 = + "https://upload.wikimedia.org/wikipedia/commons/f/ff/Prunus_cerasifera_A.jpg" - # TODO pass this response to the parser as below, with error handling + image2 = + "https://le-jardin-de-pascal.com/2195113-large_default/prunus-cerasifera-atropurpurea-prunier-myrobolan-nigra.jpg" - # temp data - response_message = Parser.parse(@plantnet_raw_response) + images = [image1, image2] - # TODO increase on success (failed id or otherwise) - # RateLimiter.increase_counter(guild_id) + query_uri = build_query_uri(images) + + case HTTPoison.get(query_uri) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + response_message = Parser.parse(body) + # RateLimiter.increase_counter(guild_id) + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: response_message + }) + + {:ok, %HTTPoison.Response{status_code: 400}} -> + # TODO add logger + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: "Bad Request" + }) + + {:ok, %HTTPoison.Response{status_code: 404}} -> + # TODO add logger + # RateLimiter.increase_counter(guild_id) + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: "Species Not Found" + }) + + {:ok, %HTTPoison.Response{status_code: 429}} -> + # TODO add logger + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: "Too Many Requests" + }) + + {_, _} -> + # TODO add logger + Api.create_followup_message(interaction.application_id, interaction.token, %{ + content: "Internal Server Error" + }) + end # TODO delete saved images - - @api.create_interaction_response(interaction, %{ - type: 4, - data: %{content: response_message <> "\n#{original_images}"} - }) end defp get_attachment_urls(interaction) do @@ -75,4 +110,18 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do @spec get_original_images([String.t()]) :: String.t() defp get_original_images(attachment_urls), do: Enum.join(attachment_urls, "\n") + + @spec build_query_uri([String.t()]) :: String.t() + defp build_query_uri(image_filenames) do + identify_api_url = "#{@plantnet_api_base_url}/identify/all?api-key=#{@plantnet_api_key}" + + query_uri = + URI.append_query( + URI.parse(identify_api_url), + "images=#{Enum.join(image_filenames, "&images=")}" + ) + |> URI.append_query("nb-results=#{@max_results}") + |> URI.append_query("type=kt") + |> URI.to_string() + end end diff --git a/lib/plantid_discord_bot/file_server/file.ex b/lib/plantid_discord_bot/file_server/file.ex index 4b313ff..b8152f5 100644 --- a/lib/plantid_discord_bot/file_server/file.ex +++ b/lib/plantid_discord_bot/file_server/file.ex @@ -56,6 +56,11 @@ defmodule PlantIdDiscordBot.FileServer.File do |> File.read() end + @spec delete_file!(String.t()) :: :ok + def delete_file!(filename) do + File.rm!(Path.join(@image_path, filename)) + end + @spec download_file!(String.t()) :: binary defp download_file!(url) do HTTPoison.get!(url)