@happyvertical/smrt-images 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/AGENTS.md +48 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +92 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/categorizer.d.ts +26 -0
  8. package/dist/categorizer.d.ts.map +1 -0
  9. package/dist/deriver.d.ts +33 -0
  10. package/dist/deriver.d.ts.map +1 -0
  11. package/dist/editor.d.ts +72 -0
  12. package/dist/editor.d.ts.map +1 -0
  13. package/dist/image.d.ts +53 -0
  14. package/dist/image.d.ts.map +1 -0
  15. package/dist/images.d.ts +80 -0
  16. package/dist/images.d.ts.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +839 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +1179 -0
  22. package/dist/media-bundle-persistence.d.ts +15 -0
  23. package/dist/media-bundle-persistence.d.ts.map +1 -0
  24. package/dist/metadata.d.ts +19 -0
  25. package/dist/metadata.d.ts.map +1 -0
  26. package/dist/playground.d.ts +2 -0
  27. package/dist/playground.d.ts.map +1 -0
  28. package/dist/playground.js +140 -0
  29. package/dist/playground.js.map +1 -0
  30. package/dist/prompts.d.ts +8 -0
  31. package/dist/prompts.d.ts.map +1 -0
  32. package/dist/search.d.ts +42 -0
  33. package/dist/search.d.ts.map +1 -0
  34. package/dist/smrt-knowledge.json +561 -0
  35. package/dist/svelte/components/AssetsGallery.svelte +436 -0
  36. package/dist/svelte/components/AssetsGallery.svelte.d.ts +11 -0
  37. package/dist/svelte/components/AssetsGallery.svelte.d.ts.map +1 -0
  38. package/dist/svelte/components/ImageEditor.svelte +485 -0
  39. package/dist/svelte/components/ImageEditor.svelte.d.ts +12 -0
  40. package/dist/svelte/components/ImageEditor.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/ImageUploader.svelte +922 -0
  42. package/dist/svelte/components/ImageUploader.svelte.d.ts +15 -0
  43. package/dist/svelte/components/ImageUploader.svelte.d.ts.map +1 -0
  44. package/dist/svelte/i18n.d.ts +42 -0
  45. package/dist/svelte/i18n.d.ts.map +1 -0
  46. package/dist/svelte/i18n.js +46 -0
  47. package/dist/svelte/image-clients.d.ts +45 -0
  48. package/dist/svelte/image-clients.d.ts.map +1 -0
  49. package/dist/svelte/image-clients.js +1 -0
  50. package/dist/svelte/index.d.ts +14 -0
  51. package/dist/svelte/index.d.ts.map +1 -0
  52. package/dist/svelte/index.js +21 -0
  53. package/dist/svelte/playground.d.ts +74 -0
  54. package/dist/svelte/playground.d.ts.map +1 -0
  55. package/dist/svelte/playground.js +105 -0
  56. package/dist/svelte/routes/ImageStudioRoute.svelte +194 -0
  57. package/dist/svelte/routes/ImageStudioRoute.svelte.d.ts +7 -0
  58. package/dist/svelte/routes/ImageStudioRoute.svelte.d.ts.map +1 -0
  59. package/dist/svelte/routes/index.d.ts +2 -0
  60. package/dist/svelte/routes/index.d.ts.map +1 -0
  61. package/dist/svelte/routes/index.js +1 -0
  62. package/dist/svelte/routes/shared.d.ts +25 -0
  63. package/dist/svelte/routes/shared.d.ts.map +1 -0
  64. package/dist/svelte/routes/shared.js +31 -0
  65. package/dist/types.d.ts +51 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +2 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/ui.d.ts +10 -0
  70. package/dist/ui.d.ts.map +1 -0
  71. package/dist/ui.js +42 -0
  72. package/dist/ui.js.map +1 -0
  73. package/dist/upstream.d.ts +65 -0
  74. package/dist/upstream.d.ts.map +1 -0
  75. package/package.json +95 -0
package/dist/index.js ADDED
@@ -0,0 +1,839 @@
1
+ import { ObjectRegistry, field, smrt, SmrtCollection } from "@happyvertical/smrt-core";
2
+ import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
3
+ import { randomUUID } from "node:crypto";
4
+ import { writeFile, readFile, unlink } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { Asset } from "@happyvertical/smrt-assets";
8
+ import { persistMediaBundleInspection } from "@happyvertical/smrt-assets";
9
+ ObjectRegistry.registerPackageManifest(
10
+ new URL("./manifest.json", import.meta.url)
11
+ );
12
+ const smrtImagesGenerateAltTextPrompt = definePrompt({
13
+ key: "smrtImages.image.generateAltText",
14
+ template: `Generate concise accessibility alt text for this image.
15
+ Consider: subject matter, key visual elements, context.
16
+ Keep it under 125 characters for screen reader compatibility.
17
+
18
+ Image name: {imageName}
19
+ Image description: {imageDescription}
20
+
21
+ Return only the alt text, with no commentary or surrounding quotation marks.`,
22
+ editable: {
23
+ template: true,
24
+ profile: true,
25
+ model: true,
26
+ params: true
27
+ }
28
+ });
29
+ function promptMessageOptions(ai) {
30
+ return {
31
+ ...ai.params || {},
32
+ ...ai.model ? { model: ai.model } : {},
33
+ ...typeof ai.temperature === "number" ? { temperature: ai.temperature } : {},
34
+ ...typeof ai.maxTokens === "number" ? { maxTokens: ai.maxTokens } : {}
35
+ };
36
+ }
37
+ class ImageCategorizer {
38
+ constructor(options) {
39
+ this.options = options;
40
+ }
41
+ options;
42
+ /**
43
+ * Categorize an image using AI vision analysis
44
+ *
45
+ * @param image - The Image instance to categorize
46
+ * @param buffer - Optional raw image data for vision analysis
47
+ * @returns Categorization results with tags, description, and subjects
48
+ */
49
+ async categorize(image, buffer) {
50
+ const { getAI } = await import("@happyvertical/ai");
51
+ const ai = await getAI(this.options.ai);
52
+ const prompt = `Analyze this image and provide categorization.
53
+ Image name: ${image.name}
54
+ Image description: ${image.description}
55
+ MIME type: ${image.mimeType}
56
+ Dimensions: ${image.width}x${image.height}
57
+
58
+ Respond in JSON format:
59
+ {
60
+ "tags": ["tag1", "tag2", ...],
61
+ "description": "Brief description of the image content",
62
+ "confidence": 0.0-1.0,
63
+ "subjects": ["subject1", "subject2", ...]
64
+ }`;
65
+ const response = await ai.chat([{ role: "user", content: prompt }]);
66
+ const text = response.content;
67
+ try {
68
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
69
+ if (jsonMatch) {
70
+ return JSON.parse(jsonMatch[0]);
71
+ }
72
+ } catch {
73
+ }
74
+ return {
75
+ tags: [],
76
+ description: image.description || image.name,
77
+ confidence: 0,
78
+ subjects: []
79
+ };
80
+ }
81
+ /**
82
+ * Run categorization and apply results to the image
83
+ *
84
+ * @param image - The Image to categorize and update
85
+ * @param assetCollection - AssetCollection for tag management
86
+ */
87
+ async autoTag(image, assetCollection) {
88
+ const result = await this.categorize(image);
89
+ if (result.description && !image.description) {
90
+ image.description = result.description;
91
+ }
92
+ if (!image.alt && result.description) {
93
+ image.alt = result.description.slice(0, 125);
94
+ }
95
+ await image.save();
96
+ for (const tag of result.tags) {
97
+ await assetCollection.addTag(image.id, tag);
98
+ }
99
+ }
100
+ }
101
+ class ImageDeriver {
102
+ constructor(store, collection, options) {
103
+ this.store = store;
104
+ this.collection = collection;
105
+ this.options = options;
106
+ }
107
+ store;
108
+ collection;
109
+ options;
110
+ /**
111
+ * Derive new images from source images and a creative prompt
112
+ *
113
+ * @param sources - One or more source images
114
+ * @param prompt - Creative instructions for generation
115
+ * @param deriveOptions - Generation options (count, size, style)
116
+ * @returns Array of newly created derivative Images
117
+ */
118
+ async derive(sources, prompt, deriveOptions = {}) {
119
+ if (sources.length === 0) {
120
+ throw new Error("At least one source image is required");
121
+ }
122
+ const { getAI } = await import("@happyvertical/ai");
123
+ const ai = await getAI(this.options.ai);
124
+ const count = deriveOptions.count ?? 1;
125
+ const results = [];
126
+ const fullPrompt = [
127
+ prompt,
128
+ deriveOptions.style ? `Style: ${deriveOptions.style}` : "",
129
+ deriveOptions.size ? `Output size: ${deriveOptions.size}` : "",
130
+ `Source images: ${sources.map((s) => s.name).join(", ")}`
131
+ ].filter(Boolean).join("\n");
132
+ for (let i = 0; i < count; i++) {
133
+ const response = await ai.generateImage(fullPrompt, {
134
+ size: deriveOptions.size
135
+ });
136
+ const imageData = response.images[0]?.data;
137
+ if (!imageData || !(imageData instanceof Buffer)) {
138
+ throw new Error("AI did not return image data as Buffer");
139
+ }
140
+ const result = imageData;
141
+ const derived = await this.collection.create({
142
+ name: `derived-${sources[0].name}-${i + 1}`,
143
+ mimeType: "image/png",
144
+ sourceUri: "",
145
+ sourceAssetId: sources[0].id,
146
+ typeSlug: "image",
147
+ description: `Derived: ${prompt}`
148
+ });
149
+ const sourceUri = await this.store.storeFile(derived, result, {
150
+ mimeType: "image/png",
151
+ typeSlug: "image"
152
+ });
153
+ derived.sourceUri = sourceUri;
154
+ await derived.save();
155
+ results.push(derived);
156
+ }
157
+ return results;
158
+ }
159
+ /**
160
+ * Derive images and link all sources via AssetAssociation
161
+ *
162
+ * @param sources - Source images
163
+ * @param prompt - Creative instructions
164
+ * @param associations - AssetAssociationCollection for linking
165
+ * @param deriveOptions - Generation options
166
+ * @returns Array of newly created derivative Images
167
+ */
168
+ async deriveWithAssociations(sources, prompt, associations, deriveOptions = {}) {
169
+ const results = await this.derive(sources, prompt, deriveOptions);
170
+ for (const derived of results) {
171
+ for (const source of sources) {
172
+ await associations.attach("Image", derived.id, source.id, {
173
+ role: "derivation-source"
174
+ });
175
+ }
176
+ }
177
+ return results;
178
+ }
179
+ }
180
+ const ALLOWED_CONVERT_FORMATS = /* @__PURE__ */ new Set([
181
+ "jpeg",
182
+ "png",
183
+ "webp",
184
+ "avif",
185
+ "gif",
186
+ "tiff"
187
+ ]);
188
+ class ImageEditor {
189
+ constructor(store, collection, options = {}) {
190
+ this.store = store;
191
+ this.collection = collection;
192
+ this.options = options;
193
+ }
194
+ store;
195
+ collection;
196
+ options;
197
+ /**
198
+ * Resize an image to the specified dimensions
199
+ *
200
+ * @param image - Source image
201
+ * @param width - Target width
202
+ * @param height - Target height
203
+ * @returns New derivative Image
204
+ */
205
+ async resize(image, width, height) {
206
+ const { resizeImage } = await import("@happyvertical/images");
207
+ const sourceData = await this.store.read(image);
208
+ const inputPath = join(tmpdir(), `smrt-edit-in-${randomUUID()}.bin`);
209
+ const outputPath = join(tmpdir(), `smrt-edit-out-${randomUUID()}.bin`);
210
+ try {
211
+ await writeFile(inputPath, sourceData);
212
+ await resizeImage(inputPath, outputPath, { width, height });
213
+ const resized = await readFile(outputPath);
214
+ return this.createDerivative(image, resized, {
215
+ name: `${image.name}-${width}x${height}`,
216
+ width,
217
+ height,
218
+ description: `Resized from ${image.width}x${image.height} to ${width}x${height}`
219
+ });
220
+ } finally {
221
+ await unlink(inputPath).catch(() => {
222
+ });
223
+ await unlink(outputPath).catch(() => {
224
+ });
225
+ }
226
+ }
227
+ /**
228
+ * Crop an image to the specified region
229
+ *
230
+ * @param image - Source image
231
+ * @param x - Left offset
232
+ * @param y - Top offset
233
+ * @param w - Crop width
234
+ * @param h - Crop height
235
+ * @returns New derivative Image
236
+ */
237
+ async crop(image, x, y, w, h) {
238
+ const { getImageProcessor } = await import("@happyvertical/images");
239
+ const processor = await getImageProcessor();
240
+ const sourceData = await this.store.read(image);
241
+ const inputPath = join(tmpdir(), `smrt-crop-in-${randomUUID()}.bin`);
242
+ const outputPath = join(tmpdir(), `smrt-crop-out-${randomUUID()}.bin`);
243
+ try {
244
+ await writeFile(inputPath, sourceData);
245
+ await processor.resize(inputPath, outputPath, {
246
+ width: w,
247
+ height: h,
248
+ fit: "cover"
249
+ });
250
+ const cropped = await readFile(outputPath);
251
+ return this.createDerivative(image, cropped, {
252
+ name: `${image.name}-crop`,
253
+ width: w,
254
+ height: h,
255
+ description: `Cropped region ${x},${y} ${w}x${h}`
256
+ });
257
+ } finally {
258
+ await unlink(inputPath).catch(() => {
259
+ });
260
+ await unlink(outputPath).catch(() => {
261
+ });
262
+ }
263
+ }
264
+ /**
265
+ * Convert an image to a different format
266
+ *
267
+ * @param image - Source image
268
+ * @param format - Target format (e.g., 'webp', 'png', 'jpeg')
269
+ * @returns New derivative Image
270
+ */
271
+ async convert(image, format) {
272
+ const normalizedFormat = format.trim().toLowerCase();
273
+ if (!ALLOWED_CONVERT_FORMATS.has(normalizedFormat)) {
274
+ throw new Error(
275
+ `Unsupported image format: ${JSON.stringify(format)}. Allowed formats: ${[...ALLOWED_CONVERT_FORMATS].join(", ")}`
276
+ );
277
+ }
278
+ const safeFormat = normalizedFormat;
279
+ const { convertFormat } = await import("@happyvertical/images");
280
+ const sourceData = await this.store.read(image);
281
+ const mimeType = `image/${safeFormat}`;
282
+ const inputPath = join(tmpdir(), `smrt-conv-in-${randomUUID()}.bin`);
283
+ const outputPath = join(
284
+ tmpdir(),
285
+ `smrt-conv-out-${randomUUID()}.${safeFormat}`
286
+ );
287
+ try {
288
+ await writeFile(inputPath, sourceData);
289
+ await convertFormat(inputPath, outputPath, {
290
+ format: safeFormat
291
+ });
292
+ const converted = await readFile(outputPath);
293
+ return this.createDerivative(image, converted, {
294
+ name: `${image.name}.${safeFormat}`,
295
+ mimeType,
296
+ description: `Converted from ${image.mimeType} to ${mimeType}`
297
+ });
298
+ } finally {
299
+ await unlink(inputPath).catch(() => {
300
+ });
301
+ await unlink(outputPath).catch(() => {
302
+ });
303
+ }
304
+ }
305
+ /**
306
+ * Generate a square thumbnail of the specified size
307
+ *
308
+ * @param image - Source image
309
+ * @param size - Thumbnail dimension (square)
310
+ * @returns New derivative Image
311
+ */
312
+ async thumbnail(image, size) {
313
+ const { generateThumbnail } = await import("@happyvertical/images");
314
+ const sourceData = await this.store.read(image);
315
+ const inputPath = join(tmpdir(), `smrt-thumb-in-${randomUUID()}.bin`);
316
+ const outputPath = join(tmpdir(), `smrt-thumb-out-${randomUUID()}.bin`);
317
+ try {
318
+ await writeFile(inputPath, sourceData);
319
+ await generateThumbnail(inputPath, outputPath, {
320
+ maxWidth: size,
321
+ maxHeight: size
322
+ });
323
+ const thumbData = await readFile(outputPath);
324
+ return this.createDerivative(image, thumbData, {
325
+ name: `${image.name}-thumb-${size}`,
326
+ width: size,
327
+ height: size,
328
+ description: `Thumbnail ${size}x${size}`
329
+ });
330
+ } finally {
331
+ await unlink(inputPath).catch(() => {
332
+ });
333
+ await unlink(outputPath).catch(() => {
334
+ });
335
+ }
336
+ }
337
+ /**
338
+ * AI-powered image generation based on a prompt (creates derivative linked to source)
339
+ *
340
+ * @param image - Source image (used for metadata, linked as parent)
341
+ * @param prompt - Generation instructions (e.g., "similar image with sunset colors")
342
+ * @returns New derivative Image
343
+ */
344
+ async edit(image, prompt) {
345
+ if (!this.options.ai) {
346
+ throw new Error("AI options required for AI-powered editing");
347
+ }
348
+ const { getAI } = await import("@happyvertical/ai");
349
+ const ai = await getAI(this.options.ai);
350
+ const response = await ai.generateImage(prompt, {
351
+ size: `${image.width}x${image.height}`
352
+ });
353
+ const imageData = response.images[0]?.data;
354
+ if (!imageData || !(imageData instanceof Buffer)) {
355
+ throw new Error("AI did not return image data as Buffer");
356
+ }
357
+ return this.createDerivative(image, imageData, {
358
+ name: `${image.name}-edited`,
359
+ description: `AI edit: ${prompt}`
360
+ });
361
+ }
362
+ /**
363
+ * Generate variations of an image using AI
364
+ *
365
+ * @param image - Source image
366
+ * @param prompt - Variation instructions
367
+ * @param options - Number of variations to generate
368
+ * @returns Array of new derivative Images
369
+ */
370
+ async generateVariation(image, prompt, options = {}) {
371
+ const count = options.count ?? 1;
372
+ const results = [];
373
+ for (let i = 0; i < count; i++) {
374
+ const variation = await this.edit(
375
+ image,
376
+ `${prompt} (variation ${i + 1} of ${count})`
377
+ );
378
+ results.push(variation);
379
+ }
380
+ return results;
381
+ }
382
+ /**
383
+ * Helper: Create a derivative Image from processed buffer data
384
+ */
385
+ async createDerivative(source, data, overrides) {
386
+ const mimeType = overrides.mimeType ?? source.mimeType;
387
+ const typeSlug = source.typeSlug || "image";
388
+ const derivative = await this.collection.create({
389
+ name: overrides.name,
390
+ sourceUri: "",
391
+ mimeType,
392
+ width: overrides.width ?? source.width,
393
+ height: overrides.height ?? source.height,
394
+ alt: source.alt,
395
+ sourceAssetId: source.id,
396
+ typeSlug,
397
+ description: overrides.description ?? ""
398
+ });
399
+ const sourceUri = await this.store.storeFile(derivative, data, {
400
+ mimeType,
401
+ typeSlug
402
+ });
403
+ derivative.sourceUri = sourceUri;
404
+ await derivative.save();
405
+ return derivative;
406
+ }
407
+ }
408
+ var __defProp = Object.defineProperty;
409
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
410
+ var __decorateClass = (decorators, target, key, kind) => {
411
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
412
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
413
+ if (decorator = decorators[i])
414
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
415
+ if (kind && result) __defProp(target, key, result);
416
+ return result;
417
+ };
418
+ let Image = class extends Asset {
419
+ width = 0;
420
+ height = 0;
421
+ alt = "";
422
+ constructor(options = {}) {
423
+ super(options);
424
+ if (options.width !== void 0) this.width = options.width;
425
+ if (options.height !== void 0) this.height = options.height;
426
+ if (options.alt !== void 0) this.alt = options.alt;
427
+ }
428
+ /**
429
+ * Calculate aspect ratio from dimensions
430
+ */
431
+ get aspectRatio() {
432
+ if (this.height === 0) return 0;
433
+ return this.width / this.height;
434
+ }
435
+ /**
436
+ * Helper to get URL from sourceUri for frontend components
437
+ */
438
+ get url() {
439
+ return this.sourceUri;
440
+ }
441
+ /**
442
+ * Check if dimensions indicate landscape orientation
443
+ */
444
+ get isLandscape() {
445
+ return this.width > this.height;
446
+ }
447
+ /**
448
+ * Check if dimensions indicate portrait orientation
449
+ */
450
+ get isPortrait() {
451
+ return this.height > this.width;
452
+ }
453
+ /**
454
+ * Check if dimensions indicate square aspect ratio
455
+ */
456
+ get isSquare() {
457
+ return this.width === this.height && this.width > 0;
458
+ }
459
+ /**
460
+ * Validate that the asset is an image based on MIME type
461
+ */
462
+ isValidImageFormat() {
463
+ return this.mimeType.startsWith("image/");
464
+ }
465
+ /**
466
+ * Check if this image is high resolution (4K+)
467
+ */
468
+ isHighResolution() {
469
+ return this.width >= 3840 || this.height >= 2160;
470
+ }
471
+ /**
472
+ * AI-powered: Generate accessibility alt text for this image.
473
+ *
474
+ * Uses the `smrtImages.image.generateAltText` prompt registered via
475
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
476
+ * overrides of the template, model, and parameters at runtime.
477
+ *
478
+ * Only non-PII metadata fields (name, description) are sent to the AI
479
+ * provider. Source URIs, internal foreign-key fields, and the
480
+ * extensible `metadata` blob are intentionally excluded — source URIs
481
+ * may embed signed/private bucket paths and metadata may contain EXIF
482
+ * GPS data or tenant-private configuration.
483
+ *
484
+ * @returns AI-generated alt text describing the image
485
+ */
486
+ async generateAltText() {
487
+ const db = this.options.db ?? this.options.persistence;
488
+ const resolvedPrompt = await resolvePrompt(
489
+ smrtImagesGenerateAltTextPrompt.key,
490
+ {
491
+ db,
492
+ tenantId: this.tenantId,
493
+ variables: {
494
+ imageName: this.name || "",
495
+ imageDescription: this.description || ""
496
+ }
497
+ }
498
+ );
499
+ const ai = await this.getAiClient();
500
+ const response = await ai.message(
501
+ resolvedPrompt.text,
502
+ promptMessageOptions(resolvedPrompt.ai)
503
+ );
504
+ const altText = response.trim();
505
+ this.alt = altText;
506
+ return altText;
507
+ }
508
+ };
509
+ __decorateClass([
510
+ field()
511
+ ], Image.prototype, "width", 2);
512
+ __decorateClass([
513
+ field()
514
+ ], Image.prototype, "height", 2);
515
+ __decorateClass([
516
+ field()
517
+ ], Image.prototype, "alt", 2);
518
+ Image = __decorateClass([
519
+ smrt({
520
+ api: { include: ["list", "get", "create", "update", "delete"] },
521
+ mcp: { include: ["list", "get", "create", "update", "generateAltText"] },
522
+ cli: true
523
+ })
524
+ ], Image);
525
+ class ImageCollection extends SmrtCollection {
526
+ static _itemClass = Image;
527
+ // ─────────────────────────────────────────────────────────────────────────────
528
+ // Tenant-Aware Query Methods
529
+ // ─────────────────────────────────────────────────────────────────────────────
530
+ /**
531
+ * Find all images belonging to a specific tenant
532
+ *
533
+ * @param tenantId - The tenant ID to filter by
534
+ * @returns Array of images belonging to this tenant
535
+ */
536
+ async findByTenant(tenantId) {
537
+ return await this.list({ where: { tenantId } });
538
+ }
539
+ /**
540
+ * Find all global images (images without a tenant)
541
+ *
542
+ * @returns Array of global images
543
+ */
544
+ async findGlobal() {
545
+ return await this.list({ where: { tenantId: null } });
546
+ }
547
+ /**
548
+ * Find images belonging to a tenant plus all global images
549
+ *
550
+ * @param tenantId - The tenant ID to include
551
+ * @returns Array of tenant-specific and global images
552
+ */
553
+ async findWithGlobals(tenantId) {
554
+ return await this.query(
555
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
556
+ [tenantId]
557
+ );
558
+ }
559
+ /**
560
+ * Get images by minimum dimensions
561
+ *
562
+ * @param minWidth - Minimum width in pixels
563
+ * @param minHeight - Minimum height in pixels
564
+ * @returns Array of images meeting minimum dimension requirements
565
+ */
566
+ async getByMinDimensions(minWidth, minHeight) {
567
+ return await this.list({
568
+ where: {
569
+ "width >=": minWidth,
570
+ "height >=": minHeight
571
+ }
572
+ });
573
+ }
574
+ /**
575
+ * Get images by maximum dimensions
576
+ *
577
+ * @param maxWidth - Maximum width in pixels
578
+ * @param maxHeight - Maximum height in pixels
579
+ * @returns Array of images within maximum dimension limits
580
+ */
581
+ async getByMaxDimensions(maxWidth, maxHeight) {
582
+ return await this.list({
583
+ where: {
584
+ "width <=": maxWidth,
585
+ "height <=": maxHeight
586
+ }
587
+ });
588
+ }
589
+ /**
590
+ * Get landscape images (width > height)
591
+ *
592
+ * @returns Array of landscape-oriented images
593
+ */
594
+ async getLandscape() {
595
+ return await this.query(
596
+ `SELECT * FROM ${this.tableName} WHERE width > height`
597
+ );
598
+ }
599
+ /**
600
+ * Get portrait images (height > width)
601
+ *
602
+ * @returns Array of portrait-oriented images
603
+ */
604
+ async getPortrait() {
605
+ return await this.query(
606
+ `SELECT * FROM ${this.tableName} WHERE height > width`
607
+ );
608
+ }
609
+ /**
610
+ * Get square images (width === height)
611
+ *
612
+ * @returns Array of square images
613
+ */
614
+ async getSquare() {
615
+ return await this.query(
616
+ `SELECT * FROM ${this.tableName} WHERE width = height AND width > 0`
617
+ );
618
+ }
619
+ /**
620
+ * Get images missing alt text
621
+ *
622
+ * @returns Array of images without accessibility text
623
+ */
624
+ async getMissingAltText() {
625
+ return await this.list({
626
+ where: { alt: "" }
627
+ });
628
+ }
629
+ /**
630
+ * Get high resolution images (4K+)
631
+ *
632
+ * @returns Array of high resolution images
633
+ */
634
+ async getHighResolution() {
635
+ return await this.query(
636
+ `SELECT * FROM ${this.tableName} WHERE width >= 3840 OR height >= 2160`
637
+ );
638
+ }
639
+ /**
640
+ * Get images by aspect ratio range
641
+ *
642
+ * @param minRatio - Minimum aspect ratio (width/height)
643
+ * @param maxRatio - Maximum aspect ratio (width/height)
644
+ * @returns Array of images within the aspect ratio range
645
+ */
646
+ async getByAspectRatio(minRatio, maxRatio) {
647
+ return await this.query(
648
+ `SELECT * FROM ${this.tableName}
649
+ WHERE height > 0
650
+ AND (CAST(width AS REAL) / height) >= ?
651
+ AND (CAST(width AS REAL) / height) <= ?`,
652
+ [minRatio, maxRatio]
653
+ );
654
+ }
655
+ }
656
+ class ImageMetadataExtractor {
657
+ /**
658
+ * Extract metadata from an image buffer
659
+ *
660
+ * @param buffer - Raw image data
661
+ * @returns Extracted metadata including dimensions and format
662
+ */
663
+ async extract(buffer) {
664
+ const { getDimensions, getImageMetadata } = await import("@happyvertical/images");
665
+ const dimensions = await getDimensions(buffer);
666
+ const metadata = await getImageMetadata(buffer);
667
+ return {
668
+ width: dimensions.width,
669
+ height: dimensions.height,
670
+ format: metadata.format ?? "",
671
+ mimeType: metadata.format ? `image/${metadata.format}` : "image/unknown",
672
+ exif: metadata.exif
673
+ };
674
+ }
675
+ /**
676
+ * Extract metadata and apply it to an Image instance
677
+ *
678
+ * @param image - The Image instance to update
679
+ * @param buffer - Raw image data
680
+ */
681
+ async extractAndApply(image, buffer) {
682
+ const result = await this.extract(buffer);
683
+ image.width = result.width;
684
+ image.height = result.height;
685
+ if (result.mimeType) image.mimeType = result.mimeType;
686
+ }
687
+ }
688
+ class ImageSearch {
689
+ constructor(collection, _options = {}) {
690
+ this.collection = collection;
691
+ this._options = _options;
692
+ }
693
+ collection;
694
+ _options;
695
+ /**
696
+ * Search images by text query with optional dimension/orientation filters
697
+ *
698
+ * @param query - Text search query
699
+ * @param searchOptions - Optional filters for dimensions, orientation, etc.
700
+ * @returns Matching images
701
+ */
702
+ async search(query, searchOptions = {}) {
703
+ const where = {};
704
+ if (searchOptions.minWidth) where["width >="] = searchOptions.minWidth;
705
+ if (searchOptions.minHeight) where["height >="] = searchOptions.minHeight;
706
+ const fetchLimit = query ? (searchOptions.limit ?? 100) * 3 : searchOptions.limit;
707
+ let results = await this.collection.list({
708
+ where,
709
+ limit: fetchLimit,
710
+ offset: searchOptions.offset
711
+ });
712
+ if (query) {
713
+ const lowerQuery = query.toLowerCase();
714
+ results = results.filter(
715
+ (img) => img.name.toLowerCase().includes(lowerQuery) || img.description?.toLowerCase().includes(lowerQuery) || img.alt?.toLowerCase().includes(lowerQuery)
716
+ );
717
+ }
718
+ if (searchOptions.orientation) {
719
+ results = results.filter((img) => {
720
+ switch (searchOptions.orientation) {
721
+ case "landscape":
722
+ return img.isLandscape;
723
+ case "portrait":
724
+ return img.isPortrait;
725
+ case "square":
726
+ return img.isSquare;
727
+ default:
728
+ return true;
729
+ }
730
+ });
731
+ }
732
+ if (searchOptions.limit && results.length > searchOptions.limit) {
733
+ results = results.slice(0, searchOptions.limit);
734
+ }
735
+ return results;
736
+ }
737
+ /**
738
+ * Find images similar to a given image
739
+ *
740
+ * @param image - The reference image
741
+ * @param options - Search options
742
+ * @returns Similar images
743
+ */
744
+ async findSimilar(image, options = {}) {
745
+ const limit = options.limit ?? 10;
746
+ const ratio = image.aspectRatio;
747
+ const minRatio = ratio * 0.8;
748
+ const maxRatio = ratio * 1.2;
749
+ const candidates = await this.collection.getByAspectRatio(
750
+ minRatio,
751
+ maxRatio
752
+ );
753
+ return candidates.filter((c) => c.id !== image.id).slice(0, limit);
754
+ }
755
+ /**
756
+ * Find images matching a natural language prompt
757
+ *
758
+ * @param prompt - Natural language description of desired images
759
+ * @param options - Search options
760
+ * @returns Matching images
761
+ */
762
+ async findByPrompt(prompt, options = {}) {
763
+ return this.search(prompt, { limit: options.limit });
764
+ }
765
+ }
766
+ class UpstreamManager {
767
+ constructor(sources, store, collection) {
768
+ this.sources = sources;
769
+ this.store = store;
770
+ this.collection = collection;
771
+ }
772
+ sources;
773
+ store;
774
+ collection;
775
+ /**
776
+ * Search across all configured upstream sources
777
+ *
778
+ * @param query - Search query
779
+ * @param options - Search options
780
+ * @returns Merged and ranked results from all sources
781
+ */
782
+ async search(query, options = {}) {
783
+ const limit = options.limit ?? 20;
784
+ const allResults = [];
785
+ const searches = this.sources.filter((s) => s.capabilities.search).map(
786
+ (source) => source.search(query, { limit }).catch(() => [])
787
+ );
788
+ const results = await Promise.all(searches);
789
+ for (const sourceResults of results) {
790
+ allResults.push(...sourceResults);
791
+ }
792
+ return allResults.slice(0, limit);
793
+ }
794
+ /**
795
+ * Import an asset from an upstream source into the local store
796
+ *
797
+ * @param sourceAsset - The upstream asset to import
798
+ * @returns Locally stored Image with provenance
799
+ */
800
+ async import(sourceAsset) {
801
+ const adapter = this.sources.find((s) => s.name === sourceAsset.sourceName);
802
+ if (!adapter) {
803
+ throw new Error(`No adapter found for source: ${sourceAsset.sourceName}`);
804
+ }
805
+ const { data, metadata } = await adapter.download(sourceAsset.externalId);
806
+ const image = await this.collection.create({
807
+ name: sourceAsset.name,
808
+ sourceUri: "",
809
+ mimeType: sourceAsset.mimeType,
810
+ width: metadata.width ?? 0,
811
+ height: metadata.height ?? 0,
812
+ alt: metadata.description ?? "",
813
+ description: metadata.attribution ? `${metadata.description ?? ""} (${metadata.attribution})` : metadata.description ?? "",
814
+ sourceType: sourceAsset.sourceName,
815
+ externalId: sourceAsset.externalId,
816
+ typeSlug: "image"
817
+ });
818
+ const sourceUri = await this.store.storeFile(image, data, {
819
+ mimeType: sourceAsset.mimeType,
820
+ typeSlug: "image"
821
+ });
822
+ image.sourceUri = sourceUri;
823
+ await image.save();
824
+ return image;
825
+ }
826
+ }
827
+ export {
828
+ Image,
829
+ ImageCategorizer,
830
+ ImageCollection,
831
+ ImageDeriver,
832
+ ImageEditor,
833
+ ImageMetadataExtractor,
834
+ ImageSearch,
835
+ UpstreamManager,
836
+ persistMediaBundleInspection as persistImageMediaBundleInspection,
837
+ smrtImagesGenerateAltTextPrompt
838
+ };
839
+ //# sourceMappingURL=index.js.map