mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-11 16:24:11 +01:00
AI: Add support for OLLAMA_BASE_URL env expansion in vision.yml #5361
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -5,4 +5,4 @@ FROM golang:1.22-alpine
|
||||
|
||||
RUN go install github.com/skibish/ddns@latest
|
||||
|
||||
CMD ["ddns", "-conf-file", "/config/ddns.yml"]
|
||||
CMD ["ddns", "-conf-file", "/config/ddns.yml"]
|
||||
|
||||
@@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=1 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
@@ -79,4 +80,4 @@ WORKDIR /photoprism
|
||||
EXPOSE 2342 2442 2443
|
||||
|
||||
# Keep container running.
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# copy scripts and debian backports sources list
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
@@ -99,4 +100,4 @@ WORKDIR /photoprism
|
||||
EXPOSE 2342 2443
|
||||
|
||||
# keep container running
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# copy scripts and debian backports sources list
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# copy scripts and debian backports sources list
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# copy scripts and debian backports sources list
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=0 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=1 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GOBIN="/usr/local/bin" \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism"
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
|
||||
# Copy scripts and package sources config.
|
||||
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
||||
|
||||
@@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=0 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=1 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=0 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
S6_KEEP_ENV=1 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
S6_KEEP_ENV=0 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
GO111MODULE="on" \
|
||||
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
S6_KEEP_ENV=1 \
|
||||
S6_VERBOSITY=0 \
|
||||
S6_LOGGING=0
|
||||
|
||||
@@ -23,4 +23,4 @@ VOLUME "/go"
|
||||
EXPOSE 8888
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/goproxy", "-listen", "0.0.0.0:8888"]
|
||||
CMD ["/goproxy", "-listen", "0.0.0.0:8888"]
|
||||
|
||||
@@ -50,6 +50,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_CPP_MIN_LOG_LEVEL=4 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434"
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -45,6 +45,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -45,6 +45,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -48,6 +48,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -48,6 +48,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
|
||||
TF_ENABLE_ONEDNN_OPTS=1 \
|
||||
MALLOC_ARENA_MAX=2 \
|
||||
PROG="photoprism" \
|
||||
OLLAMA_BASE_URL="http://ollama:11434" \
|
||||
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
|
||||
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
|
||||
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
## PhotoPrism — Vision Package
|
||||
|
||||
**Last Updated:** December 2, 2025
|
||||
**Last Updated:** December 10, 2025
|
||||
|
||||
### Overview
|
||||
|
||||
`internal/ai/vision` provides the shared model registry, request builders, and parsers that power PhotoPrism’s caption, label, face, NSFW, and future generate workflows. It reads `vision.yml`, normalizes models, and dispatches calls to one of three engines:
|
||||
|
||||
- **TensorFlow (built‑in)** — default Nasnet / NSFW / Facenet models, no remote service required.
|
||||
- **Ollama** — local or proxied multimodal LLMs. See [`ollama/README.md`](ollama/README.md) for tuning and schema details.
|
||||
- **Ollama** — local or proxied multimodal LLMs. See [`ollama/README.md`](ollama/README.md) for tuning and schema details. The engine defaults to `${OLLAMA_BASE_URL:-http://ollama:11434}/api/generate`, trimming any trailing slash on the base URL; set `OLLAMA_BASE_URL=https://ollama.com` to opt into cloud defaults.
|
||||
- **OpenAI** — cloud Responses API. See [`openai/README.md`](openai/README.md) for prompts, schema variants, and header requirements.
|
||||
|
||||
### Configuration
|
||||
@@ -93,17 +93,17 @@ The model `Options` adjust model parameters such as temperature, top-p, and sche
|
||||
|
||||
Configures the endpoint URL, method, format, and authentication for [Ollama](ollama/README.md), [OpenAI](openai/README.md), and other engines that perform remote HTTP requests:
|
||||
|
||||
| Field | Default | Notes |
|
||||
|------------------------------------|------------------------------------------|------------------------------------------------------------------------------------------|
|
||||
| `Uri` | required for remote | Endpoint base. Empty keeps model local (TensorFlow). |
|
||||
| `Method` | `POST` | Override verb if provider needs it. |
|
||||
| `Key` | `""` | Bearer token; prefer env expansion (OpenAI: `OPENAI_API_KEY`, Ollama: `OLLAMA_API_KEY`). |
|
||||
| `Username` / `Password` | `""` | Injected as basic auth when URI lacks userinfo. |
|
||||
| `Model` | `""` | Endpoint-specific override; wins over model/name. |
|
||||
| `Org` / `Project` | `""` | OpenAI headers (org/proj IDs) |
|
||||
| `RequestFormat` / `ResponseFormat` | set by engine alias | Explicit values win over alias defaults. |
|
||||
| `FileScheme` | set by engine alias (`data` or `base64`) | Controls image transport. |
|
||||
| `Disabled` | `false` | Disable the endpoint without removing the model. |
|
||||
| Field | Default | Notes |
|
||||
|------------------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `Uri` | required for remote | Endpoint base. Empty keeps model local (TensorFlow). Ollama alias fills `${OLLAMA_BASE_URL}/api/generate`, defaulting to `http://ollama:11434`. |
|
||||
| `Method` | `POST` | Override verb if provider needs it. |
|
||||
| `Key` | `""` | Bearer token; prefer env expansion (OpenAI: `OPENAI_API_KEY`, Ollama: `OLLAMA_API_KEY`). |
|
||||
| `Username` / `Password` | `""` | Injected as basic auth when URI lacks userinfo. |
|
||||
| `Model` | `""` | Endpoint-specific override; wins over model/name. |
|
||||
| `Org` / `Project` | `""` | OpenAI headers (org/proj IDs) |
|
||||
| `RequestFormat` / `ResponseFormat` | set by engine alias | Explicit values win over alias defaults. |
|
||||
| `FileScheme` | set by engine alias (`data` or `base64`) | Controls image transport. |
|
||||
| `Disabled` | `false` | Disable the endpoint without removing the model. |
|
||||
|
||||
> **Authentication:** All credentials and identifiers support `${ENV_VAR}` expansion. `Service.Key` sets `Authorization: Bearer <token>`; `Username`/`Password` injects HTTP basic authentication into the service URI when it is not already present. When `Service.Key` is empty, PhotoPrism defaults to `OPENAI_API_KEY` (OpenAI engine) or `OLLAMA_API_KEY` (Ollama engine), also honoring their `_FILE` counterparts.
|
||||
|
||||
@@ -142,7 +142,7 @@ Models:
|
||||
Engine: ollama
|
||||
Run: newly-indexed
|
||||
Service:
|
||||
Uri: http://ollama:11434/api/generate
|
||||
Uri: ${OLLAMA_BASE_URL}/api/generate
|
||||
```
|
||||
|
||||
More Ollama guidance: [`internal/ai/vision/ollama/README.md`](ollama/README.md).
|
||||
|
||||
@@ -28,25 +28,25 @@ func init() {
|
||||
}
|
||||
|
||||
// registerOllamaEngineDefaults selects the default Ollama endpoint based on the
|
||||
// available credentials and registers the engine alias accordingly. When an
|
||||
// API key is configured, we default to the hosted Cloud endpoint; otherwise we
|
||||
// assume a self-hosted instance reachable via the docker-compose default.
|
||||
// This keeps the zero-config path fast for local dev while automatically using
|
||||
// the cloud service when credentials are present.
|
||||
// configured base URL and registers the engine alias accordingly. When
|
||||
// OLLAMA_BASE_URL points at the cloud host we only switch the default model to
|
||||
// the cloud preset; the actual base URL continues to come from
|
||||
// OLLAMA_BASE_URL (or falls back to the local compose default) so we don't
|
||||
// accidentally talk to the hosted service without an explicit endpoint.
|
||||
func registerOllamaEngineDefaults() {
|
||||
defaultModel := ollama.DefaultModel
|
||||
defaultUri := ollama.DefaultUri
|
||||
ensureEnv()
|
||||
|
||||
// Detect Ollama cloud API key.
|
||||
if key := os.Getenv(ollama.APIKeyEnv); len(key) > 50 && strings.Contains(key, ".") {
|
||||
defaultModel := ollama.DefaultModel
|
||||
|
||||
// Use different default model for the Ollama cloud service.
|
||||
if baseUrl := os.Getenv(ollama.BaseUrlEnv); baseUrl == ollama.CloudBaseUrl {
|
||||
defaultModel = ollama.CloudModel
|
||||
defaultUri = ollama.CloudUri
|
||||
}
|
||||
|
||||
// Register the human-friendly engine name so configuration can simply use
|
||||
// `Engine: "ollama"` and inherit adapter defaults.
|
||||
RegisterEngineAlias(ollama.EngineName, EngineInfo{
|
||||
Uri: defaultUri,
|
||||
Uri: ollama.DefaultUri,
|
||||
RequestFormat: ApiFormatOllama,
|
||||
ResponseFormat: ApiFormatOllama,
|
||||
FileScheme: scheme.Base64,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ai/vision/ollama"
|
||||
@@ -29,6 +30,7 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("SelfHosted", func(t *testing.T) {
|
||||
ensureEnvOnce = sync.Once{}
|
||||
CaptionModel = testCaptionModel.Clone()
|
||||
_ = os.Unsetenv(ollama.APIKeyEnv)
|
||||
|
||||
@@ -56,8 +58,9 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("Cloud", func(t *testing.T) {
|
||||
ensureEnvOnce = sync.Once{}
|
||||
CaptionModel = testCaptionModel.Clone()
|
||||
t.Setenv(ollama.APIKeyEnv, cloudToken)
|
||||
t.Setenv(ollama.BaseUrlEnv, ollama.CloudBaseUrl+"/")
|
||||
|
||||
registerOllamaEngineDefaults()
|
||||
|
||||
@@ -66,8 +69,8 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) {
|
||||
t.Fatalf("expected engine info for %s", ollama.EngineName)
|
||||
}
|
||||
|
||||
if info.Uri != ollama.CloudUri {
|
||||
t.Fatalf("expected cloud uri %s, got %s", ollama.CloudUri, info.Uri)
|
||||
if info.Uri != ollama.DefaultUri {
|
||||
t.Fatalf("expected default uri %s, got %s", ollama.DefaultUri, info.Uri)
|
||||
}
|
||||
|
||||
if info.DefaultModel != ollama.CloudModel {
|
||||
@@ -78,14 +81,31 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) {
|
||||
t.Fatalf("expected caption model %s, got %s", ollama.CloudModel, CaptionModel.Model)
|
||||
}
|
||||
|
||||
if CaptionModel.Service.Uri != ollama.CloudUri {
|
||||
t.Fatalf("expected caption model uri %s, got %s", ollama.CloudUri, CaptionModel.Service.Uri)
|
||||
if CaptionModel.Service.Uri != ollama.DefaultUri {
|
||||
t.Fatalf("expected caption model uri %s, got %s", ollama.DefaultUri, CaptionModel.Service.Uri)
|
||||
}
|
||||
})
|
||||
t.Run("ApiKeyAloneKeepsLocalDefaults", func(t *testing.T) {
|
||||
ensureEnvOnce = sync.Once{}
|
||||
CaptionModel = testCaptionModel.Clone()
|
||||
t.Setenv(ollama.APIKeyEnv, cloudToken)
|
||||
|
||||
registerOllamaEngineDefaults()
|
||||
|
||||
info, ok := EngineInfoFor(ollama.EngineName)
|
||||
if !ok {
|
||||
t.Fatalf("expected engine info for %s", ollama.EngineName)
|
||||
}
|
||||
|
||||
if info.DefaultModel != ollama.DefaultModel {
|
||||
t.Fatalf("expected default model %s, got %s", ollama.DefaultModel, info.DefaultModel)
|
||||
}
|
||||
})
|
||||
t.Run("NewModels", func(t *testing.T) {
|
||||
ensureEnvOnce = sync.Once{}
|
||||
CaptionModel = testCaptionModel.Clone()
|
||||
|
||||
t.Setenv(ollama.APIKeyEnv, cloudToken)
|
||||
t.Setenv(ollama.BaseUrlEnv, ollama.CloudBaseUrl)
|
||||
registerOllamaEngineDefaults()
|
||||
|
||||
model := &Model{Type: ModelTypeCaption, Engine: ollama.EngineName}
|
||||
@@ -95,8 +115,8 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) {
|
||||
t.Fatalf("expected model %s, got %s", ollama.CloudModel, model.Model)
|
||||
}
|
||||
|
||||
if model.Service.Uri != ollama.CloudUri {
|
||||
t.Fatalf("expected service uri %s, got %s", ollama.CloudUri, model.Service.Uri)
|
||||
if model.Service.Uri != ollama.DefaultUri {
|
||||
t.Fatalf("expected service uri %s, got %s", ollama.DefaultUri, model.Service.Uri)
|
||||
}
|
||||
|
||||
if model.Service.RequestFormat != ApiFormatOllama || model.Service.ResponseFormat != ApiFormatOllama {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## PhotoPrism — Ollama Engine Integration
|
||||
|
||||
**Last Updated:** November 14, 2025
|
||||
**Last Updated:** December 10, 2025
|
||||
|
||||
### Overview
|
||||
|
||||
This package provides PhotoPrism’s native adapter for Ollama-compatible multimodal models. It lets Caption, Labels, and future Generate workflows call locally hosted models without changing worker logic, reusing the shared API client (`internal/ai/vision/api_client.go`) and result types (`LabelResult`, `CaptionResult`). Requests stay inside your infrastructure, rely on base64 thumbnails, and honor the same ACL, timeout, and logging hooks as the default TensorFlow engines.
|
||||
This package provides PhotoPrism’s native adapter for Ollama-compatible multimodal models. It lets Caption, Labels, and future Generate workflows call locally hosted models without changing worker logic, reusing the shared API client (`internal/ai/vision/api_client.go`) and result types (`LabelResult`, `CaptionResult`). Requests stay inside your infrastructure, rely on base64 thumbnails, and honor the same ACL, timeout, and logging hooks as the default TensorFlow engines. The adapter resolves `${OLLAMA_BASE_URL}/api/generate`, trimming trailing slashes and defaulting to `http://ollama:11434`; set `OLLAMA_BASE_URL=https://ollama.com` to opt into cloud defaults.
|
||||
|
||||
#### Context & Constraints
|
||||
|
||||
- Engine defaults live in `internal/ai/vision/ollama` and are applied whenever a model sets `Engine: ollama`. Aliases map to `ApiFormatOllama`, `scheme.Base64`, and a default 720 px thumbnail.
|
||||
- Engine defaults live in `internal/ai/vision/ollama` and are applied whenever a model sets `Engine: ollama`. Aliases map to `ApiFormatOllama`, `scheme.Base64`, and a default 720 px thumbnail. Cloud defaults are only selected when `OLLAMA_BASE_URL` equals `https://ollama.com`.
|
||||
- Responses may arrive as newline-delimited JSON chunks. `decodeOllamaResponse` keeps the most recent chunk, while `parseOllamaLabels` replays plain JSON strings found in `response`.
|
||||
- Structured JSON is optional for captions but enforced for labels when `Format: json` (default for label models targeting the Ollama engine).
|
||||
- The adapter never overwrites TensorFlow defaults. If an Ollama call fails, downstream code still has Nasnet, NSFW, and Face models available.
|
||||
@@ -73,6 +73,7 @@ This package provides PhotoPrism’s native adapter for Ollama-compatible multim
|
||||
- `PHOTOPRISM_VISION_YAML` — Custom `vision.yml` path. Keep it synced in Git if you automate deployments.
|
||||
- `OLLAMA_HOST`, `OLLAMA_MODELS`, `OLLAMA_MAX_QUEUE`, `OLLAMA_NUM_PARALLEL`, etc. — Provided in `compose*.yaml` to tune the Ollama daemon. Adjust `OLLAMA_KEEP_ALIVE` if you want models to stay loaded between worker batches.
|
||||
- `OLLAMA_API_KEY` / `OLLAMA_API_KEY_FILE` — Default bearer token picked up when `Service.Key` is empty; useful for hosted Ollama services (e.g., Ollama Cloud).
|
||||
- `OLLAMA_BASE_URL` — Base URL for the Ollama API; defaults to `http://ollama:11434`, trailing slashes are trimmed. Set to `https://ollama.com` to enable cloud defaults.
|
||||
- `PHOTOPRISM_LOG_LEVEL=trace` — Enables verbose request/response previews (truncated to avoid leaking images). Use temporarily when debugging parsing issues.
|
||||
|
||||
#### `vision.yml` Example
|
||||
@@ -90,7 +91,7 @@ Models:
|
||||
Stop: ["\n\n"]
|
||||
ForceJson: true
|
||||
Service:
|
||||
Uri: http://ollama:11434/api/generate
|
||||
Uri: ${OLLAMA_BASE_URL}/api/generate
|
||||
RequestFormat: ollama
|
||||
ResponseFormat: ollama
|
||||
FileScheme: base64
|
||||
@@ -102,7 +103,7 @@ Models:
|
||||
Options:
|
||||
Temperature: 0.2
|
||||
Service:
|
||||
Uri: http://ollama:11434/api/generate
|
||||
Uri: ${OLLAMA_BASE_URL}/api/generate
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
|
||||
@@ -11,10 +11,16 @@ const (
|
||||
APIKeyFileEnv = "OLLAMA_API_KEY_FILE" //nolint:gosec // environment variable name, not a secret
|
||||
// APIKeyPlaceholder is the `${VAR}` form injected when no explicit key is provided.
|
||||
APIKeyPlaceholder = "${" + APIKeyEnv + "}"
|
||||
// BaseUrlEnv defines the environment variable used for the Ollama base URL e.g. "https://ollama.com" or "http://ollama:11434".
|
||||
BaseUrlEnv = "OLLAMA_BASE_URL" //nolint:gosec // environment variable name, not a secret
|
||||
// BaseUrlPlaceholder is the `${VAR}` form injected when no explicit URL is provided.
|
||||
BaseUrlPlaceholder = "${" + BaseUrlEnv + "}"
|
||||
// DefaultBaseUrl is the local Ollama endpoint used when the environment variable is unset.
|
||||
DefaultBaseUrl = "http://ollama:11434"
|
||||
// CloudBaseUrl is the base URL for the Ollama Cloud service.
|
||||
CloudBaseUrl = "https://ollama.com"
|
||||
// DefaultUri is the default service URI for self-hosted Ollama instances.
|
||||
DefaultUri = "http://ollama:11434/api/generate"
|
||||
// CloudUri is the Ollama cloud service URI
|
||||
CloudUri = "https://ollama.com/api/generate"
|
||||
DefaultUri = BaseUrlPlaceholder + "/api/generate"
|
||||
// DefaultModel names the default caption model bundled with our adapter defaults.
|
||||
DefaultModel = "gemma3:latest"
|
||||
// CloudModel names the default caption for the Ollama cloud service, see https://ollama.com/cloud.
|
||||
|
||||
@@ -31,14 +31,18 @@ func (m *Service) Endpoint() (uri, method string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
ensureEnv()
|
||||
|
||||
if uri = strings.TrimSpace(os.ExpandEnv(m.Uri)); strings.Contains(uri, "${") {
|
||||
uri = ""
|
||||
}
|
||||
|
||||
if m.Method != "" {
|
||||
method = m.Method
|
||||
} else {
|
||||
method = ServiceMethod
|
||||
}
|
||||
|
||||
uri = strings.TrimSpace(m.Uri)
|
||||
|
||||
if username, password := m.BasicAuth(); username != "" || password != "" {
|
||||
if parsed, err := url.Parse(uri); err == nil {
|
||||
if parsed.User == nil {
|
||||
|
||||
@@ -33,10 +33,26 @@ func TestServiceEndpoint(t *testing.T) {
|
||||
wantURI: "https://keep:me@vision.example.com",
|
||||
wantMethod: ServiceMethod,
|
||||
},
|
||||
{
|
||||
name: "ExpandsBaseUrlEnv",
|
||||
svc: Service{Uri: "${OLLAMA_BASE_URL}/api/generate"},
|
||||
wantURI: "http://custom:11434/api/generate",
|
||||
wantMethod: ServiceMethod,
|
||||
},
|
||||
{
|
||||
name: "FallbacksWhenEnvMissing",
|
||||
svc: Service{Uri: "${OLLAMA_BASE_URL}/api/generate"},
|
||||
wantURI: "http://ollama:11434/api/generate",
|
||||
wantMethod: ServiceMethod,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.name == "ExpandsBaseUrlEnv" {
|
||||
t.Setenv("OLLAMA_BASE_URL", "http://custom:11434")
|
||||
}
|
||||
|
||||
uri, method := tt.svc.Endpoint()
|
||||
if uri != tt.wantURI {
|
||||
t.Fatalf("uri: got %q want %q", uri, tt.wantURI)
|
||||
|
||||
2
internal/ai/vision/testdata/vision.yml
vendored
2
internal/ai/vision/testdata/vision.yml
vendored
@@ -70,7 +70,7 @@ Models:
|
||||
Run: manual
|
||||
Resolution: 720
|
||||
Service:
|
||||
Uri: http://ollama:11434/api/generate
|
||||
Uri: ${OLLAMA_BASE_URL}/api/generate
|
||||
Key: ${OLLAMA_API_KEY}
|
||||
FileScheme: base64
|
||||
RequestFormat: ollama
|
||||
|
||||
@@ -25,49 +25,7 @@ Additional information can be found in our Developer Guide:
|
||||
package vision
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ai/vision/ollama"
|
||||
"github.com/photoprism/photoprism/internal/ai/vision/openai"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
||||
var ensureEnvOnce sync.Once
|
||||
|
||||
// ensureEnv loads environment-backed credentials once so adapters can look up
|
||||
// OPENAI_API_KEY / OLLAMA_API_KEY even when operators rely on *_FILE fallbacks.
|
||||
// Future engine integrations can reuse this hook to normalise additional
|
||||
// secrets.
|
||||
func ensureEnv() {
|
||||
ensureEnvOnce.Do(func() {
|
||||
loadEnvKeyFromFile(openai.APIKeyEnv, openai.APIKeyFileEnv)
|
||||
loadEnvKeyFromFile(ollama.APIKeyEnv, ollama.APIKeyFileEnv)
|
||||
})
|
||||
}
|
||||
|
||||
// loadEnvKeyFromFile populates envVar from fileVar when the environment value
|
||||
// is empty and the referenced file exists and is non-empty.
|
||||
func loadEnvKeyFromFile(envVar, fileVar string) {
|
||||
if os.Getenv(envVar) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := strings.TrimSpace(os.Getenv(fileVar))
|
||||
|
||||
if !fs.FileExistsNotEmpty(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
// #nosec G304 path provided via env
|
||||
if data, err := os.ReadFile(filePath); err == nil {
|
||||
if key := clean.Auth(string(data)); key != "" {
|
||||
_ = os.Setenv(envVar, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
internal/ai/vision/vision_env.go
Normal file
61
internal/ai/vision/vision_env.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ai/vision/ollama"
|
||||
"github.com/photoprism/photoprism/internal/ai/vision/openai"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
var ensureEnvOnce sync.Once
|
||||
|
||||
// ensureEnv loads environment-backed credentials once so adapters can look up
|
||||
// OPENAI_API_KEY / OLLAMA_API_KEY even when operators rely on *_FILE fallbacks.
|
||||
// Future engine integrations can reuse this hook to normalize additional
|
||||
// secrets.
|
||||
func ensureEnv() {
|
||||
ensureEnvOnce.Do(func() {
|
||||
loadEnvKeyFromFile(openai.APIKeyEnv, openai.APIKeyFileEnv)
|
||||
loadEnvKeyFromFile(ollama.APIKeyEnv, ollama.APIKeyFileEnv)
|
||||
|
||||
// Init the Ollama base URL by trimming trailing slashes or using the default.
|
||||
initEnvUrl(ollama.BaseUrlEnv, ollama.DefaultBaseUrl)
|
||||
})
|
||||
}
|
||||
|
||||
// initEnvUrl ensures that the variable contains no trailing
|
||||
// slashes and sets a default value if it is missing.
|
||||
func initEnvUrl(envName, defaultUrl string) {
|
||||
if base := strings.TrimSpace(os.Getenv(envName)); base != "" {
|
||||
if normalized := strings.TrimRight(base, "/"); normalized != base {
|
||||
_ = os.Setenv(envName, normalized)
|
||||
}
|
||||
} else if defaultUrl != "" {
|
||||
_ = os.Setenv(envName, defaultUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// loadEnvKeyFromFile populates envVar from fileVar when the environment value
|
||||
// is empty and the referenced file exists and is non-empty.
|
||||
func loadEnvKeyFromFile(envVar, fileVar string) {
|
||||
if os.Getenv(envVar) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := strings.TrimSpace(os.Getenv(fileVar))
|
||||
|
||||
if !fs.FileExistsNotEmpty(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
// #nosec G304 path provided via env
|
||||
if data, err := os.ReadFile(filePath); err == nil {
|
||||
if key := clean.Auth(string(data)); key != "" {
|
||||
_ = os.Setenv(envVar, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,31 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInitEnvUrl(t *testing.T) {
|
||||
const envName = "TEST_OLLAMA_BASE_URL"
|
||||
|
||||
// Case: trims trailing slash.
|
||||
t.Setenv(envName, "http://example.com/")
|
||||
initEnvUrl(envName, "")
|
||||
if got := os.Getenv(envName); got != "http://example.com" {
|
||||
t.Fatalf("trim: expected http://example.com, got %s", got)
|
||||
}
|
||||
|
||||
// Case: sets default when unset.
|
||||
t.Setenv(envName, "")
|
||||
initEnvUrl(envName, "http://default.local")
|
||||
if got := os.Getenv(envName); got != "http://default.local" {
|
||||
t.Fatalf("default: expected http://default.local, got %s", got)
|
||||
}
|
||||
|
||||
// Case: leaves already-normalized value untouched.
|
||||
t.Setenv(envName, "http://kept.local")
|
||||
initEnvUrl(envName, "http://ignored.local")
|
||||
if got := os.Getenv(envName); got != "http://kept.local" {
|
||||
t.Fatalf("preserve: expected http://kept.local, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadEnvKeyFromFile verifies that loadEnvKeyFromFile reads API keys from
|
||||
// *_FILE variables when the primary env var is empty.
|
||||
func TestLoadEnvKeyFromFile(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user