173 lines
5.2 KiB
Elixir
173 lines
5.2 KiB
Elixir
defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|
@moduledoc false
|
|
|
|
require Logger
|
|
|
|
alias BDS.{AI, Media, Metadata}
|
|
|
|
@doc """
|
|
Starts the image import pipeline: for each selected path, imports the file,
|
|
runs AI analysis, updates metadata, links to the post, and translates to
|
|
all configured blog languages.
|
|
|
|
Processes images with a concurrency cap via a sliding window.
|
|
"""
|
|
@spec start(list(String.t()), String.t(), String.t(), String.t(), integer(), pid()) :: :ok
|
|
def start(paths, project_id, post_id, language, concurrency_limit, parent) do
|
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
|
main_language = metadata.main_language || language
|
|
blog_languages = metadata.blog_languages || []
|
|
|
|
translate_targets =
|
|
[main_language | blog_languages]
|
|
|> Enum.reject(&(&1 == language or is_nil(&1)))
|
|
|> Enum.uniq()
|
|
|
|
{in_flight, remaining} = Enum.split(paths, concurrency_limit)
|
|
|
|
tasks =
|
|
Enum.map(in_flight, fn path ->
|
|
Task.async(fn ->
|
|
process_single_image(path, project_id, post_id, language, translate_targets, parent)
|
|
end)
|
|
end)
|
|
|
|
known_refs = MapSet.new(tasks, & &1.ref)
|
|
|
|
drain_tasks(
|
|
remaining, tasks, known_refs, project_id, post_id, language, translate_targets, parent
|
|
)
|
|
|
|
send(parent, {:add_images_complete, length(paths)})
|
|
end
|
|
|
|
defp drain_tasks(
|
|
[], tasks, _known_refs, _project_id, _post_id, _language, _translate_targets, _parent
|
|
) do
|
|
Enum.each(tasks, fn task -> Task.await(task, :infinity) end)
|
|
end
|
|
|
|
defp drain_tasks(
|
|
[next_path | rest],
|
|
tasks,
|
|
known_refs,
|
|
project_id,
|
|
post_id,
|
|
language,
|
|
translate_targets,
|
|
parent
|
|
) do
|
|
receive do
|
|
{ref, _result} when is_reference(ref) ->
|
|
if MapSet.member?(known_refs, ref) do
|
|
{_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref)
|
|
|
|
new_task =
|
|
Task.async(fn ->
|
|
process_single_image(
|
|
next_path, project_id, post_id, language, translate_targets, parent
|
|
)
|
|
end)
|
|
|
|
drain_tasks(
|
|
rest,
|
|
[new_task | remaining_tasks],
|
|
MapSet.put(MapSet.delete(known_refs, ref), new_task.ref),
|
|
project_id,
|
|
post_id,
|
|
language,
|
|
translate_targets,
|
|
parent
|
|
)
|
|
else
|
|
drain_tasks(
|
|
[next_path | rest], tasks, known_refs,
|
|
project_id, post_id, language, translate_targets, parent
|
|
)
|
|
end
|
|
|
|
{:DOWN, ref, :process, _pid, _reason} when is_reference(ref) ->
|
|
if MapSet.member?(known_refs, ref) do
|
|
{_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref)
|
|
|
|
new_task =
|
|
Task.async(fn ->
|
|
process_single_image(
|
|
next_path, project_id, post_id, language, translate_targets, parent
|
|
)
|
|
end)
|
|
|
|
drain_tasks(
|
|
rest,
|
|
[new_task | remaining_tasks],
|
|
MapSet.put(MapSet.delete(known_refs, ref), new_task.ref),
|
|
project_id,
|
|
post_id,
|
|
language,
|
|
translate_targets,
|
|
parent
|
|
)
|
|
else
|
|
drain_tasks(
|
|
[next_path | rest], tasks, known_refs,
|
|
project_id, post_id, language, translate_targets, parent
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp pop_task_by_ref(tasks, ref) do
|
|
Enum.reduce(tasks, {nil, []}, fn
|
|
%{ref: ^ref} = task, {nil, rest} -> {task, rest}
|
|
task, {found, rest} -> {found, [task | rest]}
|
|
end)
|
|
end
|
|
|
|
defp process_single_image(
|
|
path, project_id, post_id, language, translate_targets, parent
|
|
) do
|
|
with {:ok, media} <- Media.import_media(%{project_id: project_id, source_path: path}),
|
|
true <- String.starts_with?(media.mime_type || "", "image/"),
|
|
{:ok, result} <- AI.analyze_image(media.id, language: language),
|
|
{:ok, _updated} <- Media.update_media(media.id, %{
|
|
title: result.title,
|
|
alt: result.alt,
|
|
caption: result.caption
|
|
}),
|
|
{:ok, _link} <- Media.link_media_to_post(media.id, post_id) do
|
|
translate_media_translations(media.id, translate_targets)
|
|
title = result.title || media.original_name
|
|
send(parent, {:add_image_processed, title})
|
|
else
|
|
false ->
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Image pipeline error for #{path}: #{inspect(reason)}")
|
|
send(parent, {:add_image_error, path, reason})
|
|
end
|
|
end
|
|
|
|
defp translate_media_translations(_media_id, []), do: :ok
|
|
|
|
defp translate_media_translations(media_id, [target | rest]) do
|
|
case AI.translate_media(media_id, target) do
|
|
{:ok, translation} ->
|
|
Media.upsert_media_translation(media_id, target, %{
|
|
title: translation.title,
|
|
alt: translation.alt,
|
|
caption: translation.caption
|
|
})
|
|
|
|
translate_media_translations(media_id, rest)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning(
|
|
"Media translation failed for #{media_id} -> #{target}: #{inspect(reason)}"
|
|
)
|
|
|
|
translate_media_translations(media_id, rest)
|
|
end
|
|
end
|
|
end
|