first commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import type { AIProvider } from '@photo-gallery/sdk';
|
||||
|
||||
import { createClipProvider } from './clipProvider';
|
||||
import { createFaceProvider } from './faceProvider';
|
||||
import { createOCRProvider } from './ocrProvider';
|
||||
import { createTensorflowProvider } from './tensorflowProvider';
|
||||
|
||||
/**
|
||||
* The demo's AI provider, combining free in-browser capabilities + Gemini:
|
||||
* - object detection: TensorFlow.js COCO-SSD, fully in-browser (no key)
|
||||
* - face detection + recognition: face-api.js, in-browser → clustered into People
|
||||
* - OCR: tesseract.js, in-browser → searchable text + the Documents album (no key)
|
||||
* - background removal: @imgly, in-browser WASM (no key) — see generativeEdit
|
||||
* - other generative edits: Gemini, proxied through /api/ai/edit so the API key
|
||||
* stays server-side and is never exposed to the browser.
|
||||
*/
|
||||
export function createDemoAIProvider(): AIProvider {
|
||||
const tf = createTensorflowProvider();
|
||||
const face = createFaceProvider();
|
||||
const ocr = createOCRProvider();
|
||||
const clip = createClipProvider();
|
||||
|
||||
return {
|
||||
name: 'demo-ai (coco-ssd + face-api + tesseract + clip + gemini)',
|
||||
detectObjects: tf.detectObjects,
|
||||
detectFaces: face.detectFaces,
|
||||
ocr: ocr.ocr,
|
||||
embedImage: clip.embedImage,
|
||||
embedText: clip.embedText,
|
||||
|
||||
async generativeEdit(item, image, op) {
|
||||
// Remove Background runs fully in-browser (free, no key) via @imgly — works
|
||||
// even when Gemini is unavailable. Other ops go through the Gemini route.
|
||||
if (op.type === 'remove-background') {
|
||||
const inputBlob = await canvasBlob(image, 1600);
|
||||
const { removeBackground } = await import('@imgly/background-removal');
|
||||
return removeBackground(inputBlob, { output: { format: 'image/png' } });
|
||||
}
|
||||
|
||||
const { data, mimeType } = imageToBase64(image, 1280);
|
||||
const res = await fetch('/api/ai/edit', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ imageBase64: data, mimeType, op }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(err.error || `AI request failed (${res.status}).`);
|
||||
}
|
||||
const { imageBase64, mimeType: outMime } = (await res.json()) as {
|
||||
imageBase64: string;
|
||||
mimeType?: string;
|
||||
};
|
||||
return base64ToBlob(imageBase64, outMime || 'image/png');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Draw an image to a canvas (downscaled) and return base64 JPEG (no data: prefix). */
|
||||
function imageToBase64(
|
||||
image: ImageBitmap | HTMLImageElement,
|
||||
maxDim: number,
|
||||
): { data: string; mimeType: string } {
|
||||
const w = (image as HTMLImageElement).naturalWidth || (image as ImageBitmap).width;
|
||||
const h = (image as HTMLImageElement).naturalHeight || (image as ImageBitmap).height;
|
||||
const scale = Math.min(1, maxDim / Math.max(w, h));
|
||||
const cw = Math.max(1, Math.round(w * scale));
|
||||
const ch = Math.max(1, Math.round(h * scale));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = cw;
|
||||
canvas.height = ch;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Canvas not supported.');
|
||||
ctx.drawImage(image as CanvasImageSource, 0, 0, cw, ch);
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
||||
return { data: dataUrl.split(',')[1] ?? '', mimeType: 'image/jpeg' };
|
||||
}
|
||||
|
||||
/** Draw an image to a canvas (downscaled) and return a JPEG Blob. */
|
||||
function canvasBlob(image: ImageBitmap | HTMLImageElement, maxDim: number): Promise<Blob> {
|
||||
const w = (image as HTMLImageElement).naturalWidth || (image as ImageBitmap).width;
|
||||
const h = (image as HTMLImageElement).naturalHeight || (image as ImageBitmap).height;
|
||||
const scale = Math.min(1, maxDim / Math.max(w, h));
|
||||
const cw = Math.max(1, Math.round(w * scale));
|
||||
const ch = Math.max(1, Math.round(h * scale));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = cw;
|
||||
canvas.height = ch;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return Promise.reject(new Error('Canvas not supported.'));
|
||||
ctx.drawImage(image as CanvasImageSource, 0, 0, cw, ch);
|
||||
return new Promise((resolve, reject) =>
|
||||
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/jpeg', 0.92),
|
||||
);
|
||||
}
|
||||
|
||||
function base64ToBlob(base64: string, mime: string): Blob {
|
||||
const bin = atob(base64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return new Blob([bytes], { type: mime });
|
||||
}
|
||||
Reference in New Issue
Block a user