PhoenixKitAI.Images (PhoenixKitAI v0.23.0)

Copy Markdown View Source

Generic image processing over any provider: named operations composed into one edit, vision (describe an image, optionally as JSON), and a before/after check.

Everything takes an endpoint and returns bytes. The module stores nothing — the caller keeps the result wherever it keeps files (core's Storage, usually). The logged request row is the only trace.

Editing by operation

PhoenixKitAI.Images.process(endpoint, [photo_jpeg], [
  :remove_reflections,
  {:clean_background, color: "light grey"},
  {:upscale, resolution: "2K"}
])

Operations (PhoenixKitAI.Images.Operations) become one numbered instruction list, followed by a preservation clause (preserve: :subject by default — the subject, its text and logos stay as they are; :scene for room photos; false for none; or your own sentence). Options the operations imply (:remove_background wants background: "transparent" + PNG) merge under the caller's options and are then fitted to the model: an option the model does not list is dropped with a warning, or refused with strict: true. When an operation loses its implied options it falls back to its plain-prompt wording (a white background instead of a transparent one).

Options

Canonical request options, mapped per provider by the adapter: :aspect_ratio, :resolution, :size, :quality, :background, :output_format, :output_compression, :n, :seed, :response_format, :style, :mask. Control options: :model (override the endpoint's model — e.g. to compare two OpenRouter models on one endpoint), :transport, :provider_options (passed through untouched), :provider_routing (OpenRouter), :strict, :preserve, :finish, :prompt_overrides.

Endpoint defaults apply underneath: provider_settings["aspect_ratio"] / ["resolution"], and the image_size / image_quality columns.

Vision

{:ok, %{json: %{"brand" => _}}} =
  PhoenixKitAI.Images.describe(endpoint, [label_jpeg],
    prompt: "Read the label.",
    schema: %{"type" => "object", "properties" => %{"brand" => %{"type" => "string"}}})

describe/3 runs a chat completion with the images attached; with schema: (a JSON Schema map) or json: true the answer is parsed and returned under :json. compare/4 is a fixed-schema describe/3 over an original and its edit, answering whether the subject survived.

Summary

Types

Every error the image verbs return, so a caller can match exhaustively. Transport and provider errors come from PhoenixKitAI.Completion (:rate_limited, :request_timeout, {:api_error, status}, {:connection_error, reason}, …).

What process/4 returns for dry_run: true: the plan, no images.

Functions

The prompt process/4 sends for operations (normalised or not): numbered instructions, the preservation clause (preserve:), the closing line (finish: false drops it). Public so admin pages can preview it and tests can pin it.

Checks an edit against its original with a vision model: did the subject, its text and logos survive, and what changed that was not asked for. intent: describes the edit that was requested.

The question describe/3 asks when the caller gives none.

Asks a vision-capable chat endpoint about images.

Pixel dimensions from a PNG, JPEG, WebP or GIF header, without decoding.

Reads the text in images with a vision model (OCR without an OCR engine): a product label, a receipt, a sign, a page of a document.

Downloads output images a provider returned as URLs (xAI, OpenAI's response_format: "url") so callers always get bytes, and adds width / height to every image whose header can be read. A URL that cannot be fetched stays a URL, with an {:output_not_fetched, url, reason} warning. fetch_outputs: false skips the download.

Keeps options inside what the adapter can send (adapter_options) and, when there is a model listing, what the model accepts. Returns the fitted options and a warning per change, {:dropped_option, key, value}, in the canonical option order; with strict: true the first offender is an error instead.

Turns caller inputs into what adapters take: data URLs (bytes inlined) or http(s) URLs. Accepts raw bytes (type sniffed), %{data, content_type}, %{url}, and bare data: / http(s): strings. Never hand a provider a permanent URL it could cache — pass bytes.

Normalises a mask: input into the request option adapters receive (a data URL) — the OpenAI adapter sends it as the mask file, the others drop it with a {:dropped_option, :mask, _} warning. Used by process/4 and by the thin PhoenixKitAI.Completion.edit_image/4.

The option map an adapter receives: the endpoint's stored defaults underneath the caller's keyword options, canonical keys only.

Edits images (the first is the subject, the rest are references) with the given operations on endpoint. See the moduledoc.

A warning with any long binary payload replaced by its size — a dropped :mask carries a whole data URL; logs and screens get {:dropped_option, :mask, {:bytes, 12345}} instead.

The canonical request option names.

The JSON Schema extract_text/3 asks the model to fill (before fields: are added).

Types

error()

@type error() ::
  :empty_input
  | :invalid_image_input
  | :reference_image_required
  | :not_supported
  | {:image_too_large, pos_integer(), pos_integer()}
  | {:unsafe_url, String.t()}
  | {:fetch_failed, String.t(), term()}
  | {:unknown_operation, term()}
  | {:missing_parameter, atom(), atom()}
  | {:conflicting_operations, atom(), atom()}
  | {:unsupported_option, atom(), term()}
  | {:too_many_images, pos_integer(), pos_integer()}
  | {:model_not_listed, String.t()}
  | {:capabilities_unavailable, term()}
  | {:no_image_in_response, String.t() | nil}
  | {:no_json_in_response, String.t() | nil}
  | {:content_policy, String.t()}
  | atom()
  | {atom(), term()}

Every error the image verbs return, so a caller can match exhaustively. Transport and provider errors come from PhoenixKitAI.Completion (:rate_limited, :request_timeout, {:api_error, status}, {:connection_error, reason}, …).

input()

@type input() ::
  binary()
  | String.t()
  | %{data: binary(), content_type: String.t()}
  | %{url: String.t()}

plan()

@type plan() :: %{
  prompt: String.t(),
  operations: [atom()],
  warnings: [term()],
  model: String.t() | nil,
  options: map(),
  images: [],
  text: nil,
  usage: map(),
  latency_ms: 0,
  dry_run: true
}

What process/4 returns for dry_run: true: the plan, no images.

result()

@type result() :: %{
  images: [PhoenixKitAI.Provider.image()],
  text: String.t() | nil,
  usage: map(),
  latency_ms: non_neg_integer(),
  model: String.t() | nil,
  prompt: String.t(),
  operations: [atom()],
  warnings: [term()]
}

Functions

build_prompt(operations, opts \\ [])

@spec build_prompt([term()], keyword()) :: {:ok, String.t()} | {:error, term()}

The prompt process/4 sends for operations (normalised or not): numbered instructions, the preservation clause (preserve:), the closing line (finish: false drops it). Public so admin pages can preview it and tests can pin it.

compare(endpoint, before, after_image, opts \\ [])

@spec compare(PhoenixKitAI.Endpoint.t(), input(), input(), keyword()) ::
  {:ok, map()} | {:error, error()}

Checks an edit against its original with a vision model: did the subject, its text and logos survive, and what changed that was not asked for. intent: describes the edit that was requested.

Returns {:ok, %{passed: boolean, same_subject, text_and_logos_preserved, unwanted_changes, summary, usage, latency_ms, model}}.

default_question()

@spec default_question() :: String.t()

The question describe/3 asks when the caller gives none.

describe(endpoint, images, opts \\ [])

@spec describe(PhoenixKitAI.Endpoint.t(), [input()], keyword()) ::
  {:ok, map()} | {:error, error()}

Asks a vision-capable chat endpoint about images.

Options: :prompt (default "Describe this image in detail."), :system, :schema (JSON Schema map → the answer is requested and parsed as that object), :json (true → any JSON object), :model, and the chat sampling options (:temperature, :max_tokens, :top_p, :seed).

Returns {:ok, %{text, json, usage, latency_ms, model}}; json is nil unless requested. A JSON answer that does not parse is {:error, {:no_json_in_response, text}}; :on_no_json, a one-arity function, first receives the unparsed result (usage and :prompt included) so the paid call can still be logged.

dimensions(arg1)

@spec dimensions(binary()) :: {:ok, {pos_integer(), pos_integer()}} | :error

Pixel dimensions from a PNG, JPEG, WebP or GIF header, without decoding.

extract_text(endpoint, images, opts \\ [])

@spec extract_text(PhoenixKitAI.Endpoint.t(), [input()] | input(), keyword()) ::
  {:ok, map()} | {:error, error()}

Reads the text in images with a vision model (OCR without an OCR engine): a product label, a receipt, a sign, a page of a document.

Returns {:ok, %{text, blocks, language, confidence, has_illegible_text, fields, json, prompt, usage, latency_ms, model}}text is everything transcribed in reading order, blocks splits it into typed pieces (%{text, kind, language, page}, kind one of heading / paragraph / label / list / table / caption / code / handwriting / other), fields holds every requested name (nil when the model found nothing, string keys as given), has_illegible_text says some print was visible but unreadable (retake the photo rather than trust the gaps), json is the raw object. Several images are read as pages of one document, in order; page says which image a block came from.

The prompt forbids guessing: vision models will otherwise invent plausible label text (an ingredients line, a net weight) for print they cannot resolve, at full confidence.

Options: fields: — a map of name → description of a value to pull out (%{"ean" => "the barcode digits", "best_before" => "expiry date as YYYY-MM-DD"}, or a list of names), language: — a hint when the script is ambiguous, layout: :markdown — keep tables and lists as Markdown instead of plain lines, instructions: — extra wording for the prompt, schema: — replace the base schema (fields: is still added to it), plus describe/3's :model, :system and sampling options.

fetch_outputs(result, opts)

@spec fetch_outputs(map(), keyword()) :: {:ok, map()}

Downloads output images a provider returned as URLs (xAI, OpenAI's response_format: "url") so callers always get bytes, and adds width / height to every image whose header can be read. A URL that cannot be fetched stays a URL, with an {:output_not_fetched, url, reason} warning. fetch_outputs: false skips the download.

fit_options(options, model, adapter_options, strict)

@spec fit_options(
  map(),
  PhoenixKitAI.Images.ImageModel.t() | nil,
  [atom()],
  boolean()
) ::
  {:ok, map(), [term()]} | {:error, {:unsupported_option, atom(), term()}}

Keeps options inside what the adapter can send (adapter_options) and, when there is a model listing, what the model accepts. Returns the fitted options and a warning per change, {:dropped_option, key, value}, in the canonical option order; with strict: true the first offender is an error instead.

normalize_inputs(images, opts \\ [])

@spec normalize_inputs([input()], keyword()) ::
  {:ok, [String.t()]} | {:error, error()}

Turns caller inputs into what adapters take: data URLs (bytes inlined) or http(s) URLs. Accepts raw bytes (type sniffed), %{data, content_type}, %{url}, and bare data: / http(s): strings. Never hand a provider a permanent URL it could cache — pass bytes.

normalize_mask(options, mask, opts)

@spec normalize_mask(map(), input() | nil, keyword()) ::
  {:ok, map()} | {:error, error()}

Normalises a mask: input into the request option adapters receive (a data URL) — the OpenAI adapter sends it as the mask file, the others drop it with a {:dropped_option, :mask, _} warning. Used by process/4 and by the thin PhoenixKitAI.Completion.edit_image/4.

options(endpoint, opts)

@spec options(PhoenixKitAI.Endpoint.t(), keyword() | map()) :: map()

The option map an adapter receives: the endpoint's stored defaults underneath the caller's keyword options, canonical keys only.

process(endpoint, images, operations, opts \\ [])

@spec process(PhoenixKitAI.Endpoint.t(), [input()], [term()], keyword()) ::
  {:ok, result() | plan()} | {:error, error()}

Edits images (the first is the subject, the rest are references) with the given operations on endpoint. See the moduledoc.

redact_warning(warning)

@spec redact_warning(term()) :: term()

A warning with any long binary payload replaced by its size — a dropped :mask carries a whole data URL; logs and screens get {:dropped_option, :mask, {:bytes, 12345}} instead.

request_options()

@spec request_options() :: [atom()]

The canonical request option names.

text_schema()

@spec text_schema() :: map()

The JSON Schema extract_text/3 asks the model to fill (before fields: are added).