- TypeScript 94.6%
- JavaScript 5.4%
512 tokens was exhausted by reasoning_content on thinking models (e.g. gemma-4-26B), leaving message.content empty. 4096 gives enough headroom for both reasoning and the actual description. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| prisma | ||
| prompts | ||
| src | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| .oxlintrc.json | ||
| .prettierignore | ||
| .prettierrc.json | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| compose.yml | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| TODO.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
recall
Small CLI tool that analyzes a folder of images using a local vision LLM and makes them searchable by natural-language description.
Purpose
Point the tool at a folder of images. Each image is sent to a local vision LLM (via an OpenAI-compatible HTTP endpoint, e.g. vLLM or Ollama serving gemma3:4b). The model produces a text summary, which is stored in SQLite and indexed in Meilisearch. Later you can query Meilisearch (CLI now, webapp later) to find images by content.
Status
Complete and working prototype.
- Full pipeline implemented: walk → hash → dedup → LLM → SQLite → Meilisearch
- 7 test suites, 41 tests (unit + integration), all independently runnable
- CLI commands:
analyze,reindex,search - Config via
.env, Meilisearch viapodman-compose
Not yet implemented (see TODO.md): webapp, HEIC/WebP/RAW formats, parallel workers, watch mode, structured LLM output.
Stack
- Node.js + TypeScript
- LLM: OpenAI-compatible HTTP API (vLLM/Ollama), vision-capable model
- Image processing:
sharp - Database: SQLite via Prisma ORM (
prisma+@prisma/client) - Search: Meilisearch (run via
podman-compose) - CLI:
commander - Config:
dotenv+zod - Lint/format/test:
oxlint,prettier,tsc,vitest
Repository Structure
image-analyzer/
├── src/
│ ├── cli.ts # commander entry
│ ├── config.ts # .env load + zod validation
│ ├── db/
│ │ └── index.ts # Prisma client singleton
├── prisma/
│ └── schema.prisma # data model + SQLite datasource
│ ├── ingest/
│ │ ├── walk.ts # folder walk + ext filter
│ │ └── hash.ts # sha256 of file
│ ├── llm/
│ │ ├── client.ts # OpenAI-compat client
│ │ └── prompt.ts # default prompt, configurable
│ ├── analyze/
│ │ └── pipeline.ts # ingest → hash → skip? → llm → db → meili
│ ├── meili/
│ │ └── index.ts # client + sync from sqlite
│ └── commands/
│ ├── analyze.ts
│ ├── reindex.ts
│ └── search.ts
├── tests/
├── compose.yml # podman-compose, Meilisearch service
├── .env.example
├── TODO.md
├── AGENTS.md / CLAUDE.md
└── package.json
Configuration
All configuration via .env. See .env.example.
| Key | Purpose |
|---|---|
LLM_BASE_URL |
OpenAI-compatible endpoint (e.g. http://192.168.1.179/v1) |
LLM_API_KEY |
API key (dummy value OK for local/unauth) |
LLM_MODEL |
Model name (e.g. gemma3:4b) |
LLM_PROMPT_FILE |
Path to file containing prompt template |
IMAGE_DIR |
Folder to analyze |
RECURSIVE |
true/false — recurse subfolders |
DATABASE_URL |
SQLite path (e.g. file:./data/images.db) — read by Prisma |
MEILI_URL |
Meilisearch URL |
MEILI_KEY |
Meilisearch master key |
CLI
recall analyze [--force] [--dir <path>] # walk + analyze
recall reindex # rebuild Meili index from sqlite
recall search <query> # query Meili
analyze skips images whose content hash already exists in the DB unless --force is passed.
Setup
npm install
cp .env.example .env
# edit .env
npx prisma migrate dev # creates SQLite DB + runs migrations
podman-compose up -d # starts Meilisearch
npm run dev analyze
Verification
npm run lint
npm run format
npm run typecheck
npm run test
Conventions
See AGENTS.md (CLAUDE.md is a symlink to it).