@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.
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:
ImageextendsAsset(shared table, qualified discriminator) - AI alt-text via
generateAltText()through the smrt-prompts registry ImageCategorizer: AI vision analysis → tags, description, confidence, subjectsImageEditor: resize / crop / convert / thumbnail / AI edits — derivatives linked viaparentIdImageDeriver: AI-generated derivations; records source provenance viaAssetAssociationImageMetadataExtractor: dimensions, format, EXIF (via@happyvertical/images)ImageSearch: text search with orientation filtersUpstreamManager: import from external providers with provenance tracking- UI registry:
assets-gallery,image-editor,image-uploaderslots
Installation
pnpm add @happyvertical/smrt-imagesnpm install @happyvertical/smrt-imagesQuick Start
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.
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
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
// 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().
import { smrtImagesGenerateAltTextPrompt } from '@happyvertical/smrt-images';
// key: 'smrtImages.image.generateAltText'PII contract
Only non-PII metadata fields are passed to the AI provider:
| Allow-listed | Excluded | Reason |
|---|---|---|
name | sourceUri | May embed signed/private bucket paths |
description | parentId, tenantId | Internal 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.
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 ID | Component | Purpose |
|---|---|---|
assets-gallery | AssetsGallery.svelte | Image gallery list |
image-editor | ImageEditor.svelte | Inline editor surface |
image-uploader | ImageUploader.svelte | Upload 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
AssetRuntimeacross 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
metadatablobs to AI prompts (may contain EXIF GPS / tenant-private data)