PHPackages                             justinholtweb/craft-spectacles - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Search &amp; Filtering](/categories/search)
4. /
5. justinholtweb/craft-spectacles

ActiveCraft-plugin[Search &amp; Filtering](/categories/search)

justinholtweb/craft-spectacles
==============================

Computer-vision-powered similar-image search for Craft CMS assets.

00PHP

Since Jun 11Pushed 5d agoCompare

[ Source](https://github.com/justinholtweb/craft-spectacles)[ Packagist](https://packagist.org/packages/justinholtweb/craft-spectacles)[ RSS](/packages/justinholtweb-craft-spectacles/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)DependenciesVersions (1)Used By (0)

Spectacles
==========

[](#spectacles)

Computer-vision-powered similar-image search for Craft CMS.

Docs and support:

Spectacles analyzes your asset library with a vision model, stores structured metadata and a similarity vector for each image, and exposes endpoints + Twig helpers for finding similar images. Visitors can upload an image and get back the closest matches from your library.

Requirements
------------

[](#requirements)

- Craft CMS 5.0+
- PHP 8.2+
- An API key from one of the supported providers — or a local Ollama daemon

Installation
------------

[](#installation)

```
composer require justinholtweb/craft-spectacles
./craft plugin/install spectacles
```

Supported providers
-------------------

[](#supported-providers)

Vision and embedding are configured independently — pick whichever combination suits your budget and quality bar.

ProviderVisionText embeddingImage embeddingNotes**OpenAI**gpt-4o, gpt-4o-mini, gpt-4.1text-embedding-3-small / -large—Strong default. JSON mode is reliable.**Anthropic**Claude 4.x (Opus / Sonnet / Haiku)——Highest-quality descriptions. Pair with another embedder.**Google Gemini**gemini-2.5-flash, gemini-2.5-protext-embedding-004—Fast and inexpensive.**Voyage AI**—voyage-multimodal-3voyage-multimodal-3**Best similarity results** — embeds the image directly, no description-loss.**Ollama (local)**llava, llama3.2-vision, bakllavanomic-embed-text, mxbai-embed-large—Self-hosted, no API costs, slower.**Recommended pairings:**

- *Best quality:* Anthropic Claude (vision) + Voyage `voyage-multimodal-3` (embedding)
- *Best price/perf:* OpenAI `gpt-4o-mini` + OpenAI `text-embedding-3-small`
- *Self-hosted:* Ollama `llava` + Ollama `nomic-embed-text`
- *Strict similarity, weak descriptions OK:* any vision provider + Voyage multimodal

When the embedding provider is multimodal-capable (currently only Voyage), Spectacles will embed the **image bytes directly** instead of embedding the text description — this captures visual features the description would lose.

Configuration
-------------

[](#configuration)

Settings live at **Settings → Plugins → Spectacles**. Set API keys via environment variables and reference them as `$OPENAI_API_KEY`, `$ANTHROPIC_API_KEY`, `$GEMINI_API_KEY`, `$VOYAGE_API_KEY`.

Other settings:

- `autoAnalyzeOnUpload` — queue an analysis job whenever an image asset is saved.
- `volumeUids` — restrict analysis to specific volumes.
- `defaultResultLimit` / `minSimilarityScore` — search tuning.
- `allowPublicSearch` / `maxVisitorUploadKb` — public upload endpoint controls.

> **Switching providers?** Different models produce vectors of different dimensions, so Spectacles only compares vectors of matching shape. After switching, click **Re-index all images** to regenerate embeddings.

Usage
-----

[](#usage)

### Re-index existing assets

[](#re-index-existing-assets)

From the settings screen, click **Re-index all images** to queue a job for every image in the configured volumes. Progress is visible in the queue.

### Twig

[](#twig)

```
{# similar to a given asset #}
{% set similar = craft.spectacles.similar(asset, 8) %}
{% for row in similar %}

        {{ row.score }} — {{ row.metadata.description }}

{% endfor %}

{# free-form text search #}
{% for row in craft.spectacles.searchText('foggy mountain at sunrise') %}

{% endfor %}
```

### Visitor upload form

[](#visitor-upload-form)

Drop the included partial into any frontend template:

```
{% include 'spectacles/_partials/upload-form' %}
```

Or post to the endpoint directly:

```
POST /spectacles/search
Content-Type: multipart/form-data
Accept: application/json

image=
```

Response:

```
{
  "analysis": {
    "description": "A foggy mountain ridge at sunrise.",
    "tags": ["mountain", "fog", "sunrise"],
    "objects": ["mountain", "trees"],
    "colors": ["pink", "blue", "gray"]
  },
  "results": [
    { "id": 412, "url": "...", "thumbUrl": "...", "score": 0.87, "description": "..." }
  ]
}
```

### JSON for an existing asset

[](#json-for-an-existing-asset)

```
GET /spectacles/similar/{assetId}
Accept: application/json
```

Architecture
------------

[](#architecture)

- `spectacles_imagemetadata` table stores description, tags, objects, colors, and the embedding vector (as JSON) for each asset.
- Vision and embedding are separate concerns:
    - `services/vision/VisionProvider` — `analyze()` returns an `AnalysisResult`.
    - `services/embedding/EmbeddingProvider` — `embedText()` and optional `embedImage()` return `EmbeddingResult`.
- `services/Vision::embedForImage()` prefers multimodal embedding when the provider supports it, falling back to text.
- Similarity uses pluggable backends (`services/similarity/SimilarityBackend`):
    - **Scan** (default): cosine in PHP over the JSON embeddings; works on any DB.
    - **pgvector**: queries a dedicated `spectacles_imagevectors` table with the `vector` type; orders of magnitude faster and lets you add an HNSW or IVFFlat index for production scale.
    - The active backend is auto-detected on Postgres + extension, or you can force it from the settings screen.

### CP integration

[](#cp-integration)

The asset edit screen renders a Spectacles panel in the sidebar showing the description, tags, similar-image thumbnails, and the active backend/model. If the asset hasn't been analyzed yet, an "Analyze now" button queues a job.

### Adding a provider

[](#adding-a-provider)

1. Implement `VisionProvider` and/or `EmbeddingProvider` under `src/services/vision/` or `src/services/embedding/`.
2. Add a constant to `Settings::PROVIDER_*` and to `VISION_PROVIDERS` / `EMBEDDING_PROVIDERS`.
3. Wire it into `Vision::visionProvider()` / `Vision::embeddingProvider()`.
4. Add fields to the settings template.

Development
-----------

[](#development)

The repo ships a [DDEV](https://ddev.com) config so the toolchain runs without installing PHP locally:

```
ddev start
ddev composer install
ddev test              # everything: unit + integration + static analysis
```

`ddev test` also accepts a target: `ddev test unit`, `ddev test integration`, or `ddev test phpstan`.

### Test suites

[](#test-suites)

There are two, split by what they need to run:

SuiteRunnerLocationScope`unit`PHPUnit`tests/unit`Pure logic — cosine similarity, provider response normalization, JSON extraction, pgvector literal formatting. No Craft, no database.`integration`Codeception + Craft's test framework`tests/integration`Anything needing a booted Craft: settings validation, the ActiveRecord and its JSON columns, the similarity/metadata/vision services, the Twig variable, event wiring, and the public endpoint's rate limiter.Without DDEV:

```
composer install
composer test              # PHPUnit only
composer test:integration  # Codeception (needs a database)
composer phpstan
```

The integration suite installs Craft into a dedicated `craft_test` database and wipes it on every run — it never touches the project database. DDEV provisions that database via a `post-start` hook; outside DDEV, create it yourself and point `tests/.env` at it.

License
-------

[](#license)

This plugin is licensed under the [Craft license](LICENSE.md). See `LICENSE.md`for the full terms.

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance65

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/035cb655c55af0e9e5b96754b80fd9703e195c32dbdfc49ae9a43ab9cf8db560?d=identicon)[justinholtweb](/maintainers/justinholtweb)

---

Top Contributors

[![justinholtweb](https://avatars.githubusercontent.com/u/295903?v=4)](https://github.com/justinholtweb "justinholtweb (8 commits)")

### Embed Badge

![Health badge](/badges/justinholtweb-craft-spectacles/health.svg)

```
[![Health](https://phpackages.com/badges/justinholtweb-craft-spectacles/health.svg)](https://phpackages.com/packages/justinholtweb-craft-spectacles)
```

###  Alternatives

[awesome-nova/dependent-filter

Dependent filters for Laravel Nova

26193.1k](/packages/awesome-nova-dependent-filter)[algolia/php-dom-parser

A simple tool to turn DOM into Algolia search friendly record objects.

181.8k](/packages/algolia-php-dom-parser)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
