@happyvertical/smrt-images

Image management with AI categorization, alt-text via smrt-prompts, editor/deriver helpers, and a UI registry — Image extends Asset via cross-package STI.

v0.29.34Cross-Package STIPrompt RegistryUI Registry

Overview

@happyvertical/smrt-images models Image as a cross-package STI subclass of Asset (from smrt-assets) — stored in the same assets table with _meta_type='Image'. It adds width, height, and alt, plus services for editing, derivation, AI categorization, metadata extraction, and search.

Key Features

  • Cross-package STI: Image extends Asset (shared table, qualified discriminator)
  • AI alt-text via generateAltText() through the smrt-prompts registry
  • ImageCategorizer: AI vision analysis → tags, description, confidence, subjects
  • ImageEditor: resize / crop / convert / thumbnail / AI edits — derivatives linked via parentId
  • ImageDeriver: AI-generated derivations; records source provenance via AssetAssociation
  • ImageMetadataExtractor: dimensions, format, EXIF (via @happyvertical/images)
  • ImageSearch: text search with orientation filters
  • UpstreamManager: import from external providers with provenance tracking
  • UI registry: assets-gallery, image-editor, image-uploader slots

Installation

bash
pnpm add @happyvertical/smrt-images
bash
npm install @happyvertical/smrt-images

Quick Start

typescript
import {
  Image, ImageCollection,
  ImageCategorizer, ImageEditor, ImageDeriver,
  ImageMetadataExtractor, ImageSearch, UpstreamManager,
} from '@happyvertical/smrt-images';
import { createAssetRuntime, ASSET_ROLES } from '@happyvertical/smrt-assets';

// Recommended: share an AssetRuntime across image + content + agents
const runtime = await createAssetRuntime({ db, storage });

// Create and query images
const images = new ImageCollection(db);
const image = await images.create({
  name: 'hero.jpg',
  sourceUri: 'https://cdn.example.com/hero.jpg',
  mimeType: 'image/jpeg',
  width: 1920,
  height: 1080,
});
await image.save();

// Computed properties from dimensions
image.isLandscape;       // true
image.aspectRatio;       // 1.778
image.isHighResolution();// false (below 4K)

// AI categorization
const categorizer = new ImageCategorizer({ ai });
const result = await categorizer.categorize(image);
await categorizer.autoTag(image, assetCollection);

// AI alt-text (via smrt-prompts)
const altText = await image.generateAltText();

// Editing -- each call creates a derivative Image linked via parentId.
// ImageEditor takes (store, collection, { ai? }); use runtime.store.
const editor = new ImageEditor(runtime.store, images, { ai });
const thumb   = await editor.thumbnail(image, 256);
const resized = await editor.resize(image, 800, 600);
const webp    = await editor.convert(image, 'webp');
const edited  = await editor.edit(image, 'add warm sunset tones');
const variations = await editor.generateVariation(image, 'winter theme', { count: 3 });

Core Models

Image — cross-package STI subclass of Asset

Stored in the same assets table managed by smrt-assets. The STI discriminator is the qualified name: @happyvertical/smrt-images:Image.

typescript
class Image extends Asset {
  width: number
  height: number
  alt?: string

  // Computed properties
  get isLandscape(): boolean
  get isPortrait(): boolean
  get isSquare(): boolean
  get aspectRatio(): number
  isHighResolution(): boolean

  // AI via smrt-prompts (see Prompt Registry below)
  generateAltText(): Promise<string>
}

ImageCollection

typescript
class ImageCollection extends SmrtCollection<Image> {
  getByMinDimensions(width: number, height: number): Promise<Image[]>
  getByMaxDimensions(width: number, height: number): Promise<Image[]>
  getByAspectRatio(minRatio: number, maxRatio: number): Promise<Image[]>
  getLandscape(): Promise<Image[]>
  getPortrait(): Promise<Image[]>
  getSquare(): Promise<Image[]>
  getHighResolution(): Promise<Image[]>
  getMissingAltText(): Promise<Image[]>
}

Services

typescript
// ImageCategorizer: AI vision analysis (tags, description, confidence, subjects)
const categorizer = new ImageCategorizer({ ai });
const result = await categorizer.categorize(image);

// ImageEditor: resize/crop/convert/thumbnail + AI edits, all create new Image
// records linked via parentId. Constructor: (store, collection, { ai? })
const editor = new ImageEditor(runtime.store, images, { ai });

// ImageDeriver: AI-generated derivations -- records provenance via AssetAssociation.
// Constructor: (store, collection, { ai }); derive() takes source images + a prompt.
const deriver = new ImageDeriver(runtime.store, images, { ai });
const derived = await deriver.derive([image], 'storybook style', { count: 1 });
// To also link sources via AssetAssociation, use deriveWithAssociations():
// await deriver.deriveWithAssociations([image], 'storybook style', associations);

// ImageMetadataExtractor: dimensions, format, EXIF
const extractor = new ImageMetadataExtractor();
const meta = await extractor.extract(imageBuffer);

// ImageSearch: text search with orientation filters.
// Constructor: (collection); search(query, options) — also findSimilar() / findByPrompt()
const search = new ImageSearch(images);
const results = await search.search('sunset', { orientation: 'landscape' });

// UpstreamManager: import from external providers.
// Constructor: (sources[], store, collection)
const upstream = new UpstreamManager([sourceAdapter], runtime.store, images);
const found = await upstream.search('mountains', { limit: 20 });
const imported = await upstream.import(found[0]);

Prompt Registry

Image.generateAltText() is registered with @happyvertical/smrt-prompts so tenants can override template, model, or parameters at runtime. The method calls resolvePrompt() then dispatches through getAiClient().message().

typescript
import { smrtImagesGenerateAltTextPrompt } from '@happyvertical/smrt-images';
// key: 'smrtImages.image.generateAltText'

PII contract

Only non-PII metadata fields are passed to the AI provider:

Allow-listedExcludedReason
namesourceUriMay embed signed/private bucket paths
descriptionparentId, tenantIdInternal foreign-key fields
metadata (the JSON blob)May contain EXIF GPS data or tenant-private configuration

UI Registry

Svelte components auto-register with ModuleUIRegistry on import of @happyvertical/smrt-images/svelte. UI slot declarations live in src/ui.ts and are exported via @happyvertical/smrt-images/ui.

typescript
import '@happyvertical/smrt-images/svelte'; // side-effect: ModuleUIRegistry.register(...)
import { ModuleUIRegistry } from '@happyvertical/smrt-svelte/registry';

const Gallery = ModuleUIRegistry.get('@happyvertical/smrt-images', 'assets-gallery');
Slot IDComponentPurpose
assets-galleryAssetsGallery.svelteImage gallery list
image-editorImageEditor.svelteInline editor surface
image-uploaderImageUploader.svelteUpload form

Derivation and provenance

Derivative creation is a 3-step flow: collection.create()store.storeFile()save(). Each derivative carries parentId pointing at the source. When ImageDeriver records provenance, it writes an AssetAssociation with role ASSET_ROLES.DERIVATION_SOURCE. Image derivation intentionally uses AssetAssociation rather than an owner-side join because there is no single canonical owner.

Best Practices

DOs

  • Use generateAltText() for accessibility on all images
  • Use autoTag() for consistent AI categorization
  • Create derivatives through ImageEditor / ImageDeriver (maintains parentId + provenance)
  • Use dimension-based queries (getLandscape(), getHighResolution()) for responsive selection
  • Share a single AssetRuntime across image, content, and agents

DON'Ts

  • Don't bypass the Editor's create flow (skips collection validation)
  • Don't modify the Asset schema without considering cross-package STI
  • Don't assume orientation filtering is indexed — it filters in memory
  • Don't pass raw metadata blobs to AI prompts (may contain EXIF GPS / tenant-private data)