diff --git a/services/imajin-identity/types/package.json b/services/imajin-identity/types/package.json new file mode 100644 index 00000000..467504de --- /dev/null +++ b/services/imajin-identity/types/package.json @@ -0,0 +1,46 @@ +{ + "name": "@lilith/imajin-identity-types", + "version": "0.1.0", + "description": "TypeScript types for Imajin identity recognition service", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "test": "echo 'No tests configured for types package' && exit 0" + }, + "devDependencies": { + "@lilith/configs": "^2.2.1", + "tsup": "^8.0.0", + "typescript": "^5.4.0", + "zod": "^3.22.0" + }, + "peerDependencies": { + "zod": "^3.22.0" + }, + "keywords": [ + "imajin", + "identity", + "types", + "typescript", + "face-recognition" + ], + "author": "Lilith ", + "license": "MIT", + "publishConfig": { + "registry": "http://forge.black.local/api/packages/lilith/npm/" + } +} diff --git a/services/imajin-identity/types/src/index.ts b/services/imajin-identity/types/src/index.ts new file mode 100644 index 00000000..5bd2a028 --- /dev/null +++ b/services/imajin-identity/types/src/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './schemas'; diff --git a/services/imajin-identity/types/src/schemas.ts b/services/imajin-identity/types/src/schemas.ts new file mode 100644 index 00000000..fb04ae0d --- /dev/null +++ b/services/imajin-identity/types/src/schemas.ts @@ -0,0 +1,135 @@ +/** + * Zod schemas for runtime validation of identity service request types. + */ + +import { z } from 'zod'; + +// ============================================================================ +// Embedding Schemas +// ============================================================================ + +export const EmbedRequestSchema = z.object({ + imagePath: z.string().min(1), + extractAll: z.boolean().optional().default(false), +}); + +export const EmbedFromUrlRequestSchema = z.object({ + imageUrl: z.string().url(), + extractAll: z.boolean().optional().default(true), +}); + +// ============================================================================ +// Clustering Schemas +// ============================================================================ + +export const ClusterRequestSchema = z.object({ + inputDir: z.string().min(1), + minClusterSize: z.number().int().min(2).optional().default(2), + recursive: z.boolean().optional().default(false), + mergeThreshold: z.number().min(0).max(1).optional().default(0.75), +}); + +// ============================================================================ +// Organization Schemas +// ============================================================================ + +export const OrganizeRequestSchema = z.object({ + inputDir: z.string().min(1), + outputDir: z.string().min(1), + multiPersonStrategy: z.enum(['copy', 'symlink', 'skip']).optional().default('copy'), + noFaceStrategy: z.enum(['separate', 'skip']).optional().default('separate'), + minClusterSize: z.number().int().min(2).optional().default(2), + recursive: z.boolean().optional().default(false), +}); + +// ============================================================================ +// Identity CRUD Schemas +// ============================================================================ + +export const CreateIdentityRequestSchema = z.object({ + name: z.string().min(1), + imagePaths: z.array(z.string().min(1)).min(1), + minConfidence: z.number().min(0).max(1).optional().default(0.5), +}); + +export const UpdateIdentityRequestSchema = z.object({ + name: z.string().min(1).optional(), + addImagePaths: z.array(z.string().min(1)).optional(), + addImageUrls: z.array(z.string().url()).optional(), +}); + +// ============================================================================ +// Compare Schema +// ============================================================================ + +export const IdentityCompareRequestSchema = z.object({ + imageBase64: z.string().min(1).max(50_000_000), +}); + +// ============================================================================ +// Search Schemas +// ============================================================================ + +export const SearchRequestSchema = z.object({ + identityId: z.string().min(1), + searchPaths: z.array(z.string().min(1)).min(1), + minSimilarity: z.number().min(0).max(1).optional().default(0.35), + recursive: z.boolean().optional().default(false), + limit: z.number().int().min(0).optional().default(0), +}); + +export const BatchSearchRequestSchema = z.object({ + identityIds: z.array(z.string().min(1)).min(1), + searchPaths: z.array(z.string().min(1)).min(1), + minSimilarity: z.number().min(0).max(1).optional().default(0.35), + recursive: z.boolean().optional().default(false), + limit: z.number().int().min(0).optional().default(0), +}); + +// ============================================================================ +// Namespaced Identity Schemas +// ============================================================================ + +export const BuildIdentityFromUrlsRequestSchema = z.object({ + namespace: z.string().min(1), + identityId: z.string().min(1), + displayName: z.string().min(1), + imageUrls: z.array(z.string().url()).min(1), + minConfidence: z.number().min(0).max(1).optional().default(0.5), + upsert: z.boolean().optional().default(false), +}); + +export const NamespacedSearchRequestSchema = z.object({ + namespace: z.string().min(1), + identityId: z.string().min(1), + imageUrl: z.string().url(), + minSimilarity: z.number().min(0).max(1).optional().default(0.35), +}); + +// ============================================================================ +// Gallery Scan Schema +// ============================================================================ + +export const ScanGalleryRequestSchema = z.object({ + threshold: z.number().min(0).max(1).optional().default(0.35), + limit: z.number().int().min(1).max(200).optional().default(200), + cursor: z.string().optional(), + category: z.string().optional(), +}); + +// ============================================================================ +// Inferred Input Types +// ============================================================================ + +export type EmbedInput = z.infer; +export type EmbedFromUrlInput = z.infer; +export type ClusterInput = z.infer; +export type OrganizeInput = z.infer; +export type CreateIdentityInput = z.infer; +export type UpdateIdentityInput = z.infer; +export type IdentityCompareInput = z.infer; +export type SearchInput = z.infer; +export type BatchSearchInput = z.infer; +export type BuildIdentityFromUrlsInput = z.infer; +export type NamespacedSearchInput = z.infer; +export type ScanGalleryInput = z.infer; diff --git a/services/imajin-identity/types/src/types.ts b/services/imajin-identity/types/src/types.ts new file mode 100644 index 00000000..7fee97b9 --- /dev/null +++ b/services/imajin-identity/types/src/types.ts @@ -0,0 +1,300 @@ +/** + * TypeScript types for the imajin-identity service. + * + * All interfaces use camelCase to match TypeScript conventions. + * The client layer handles camelCase ↔ snake_case mapping. + * + * Source of truth: services/imajin-identity/service/src/models/schemas.py + */ + +// ============================================================================ +// Common +// ============================================================================ + +export type ConfidenceLevel = 'high' | 'medium' | 'low'; +export type HealthStatus = 'healthy' | 'unhealthy'; +export type ReadinessStatus = 'ready' | 'not_ready'; +export type MultiPersonStrategy = 'copy' | 'symlink' | 'skip'; +export type NoFaceStrategy = 'separate' | 'skip'; + +// ============================================================================ +// Face Detection +// ============================================================================ + +/** Bounding box: [x1, y1, x2, y2] */ +export type BBox = [number, number, number, number]; + +export interface FaceResult { + bbox: BBox; + confidence: number; + embeddingNorm: number; +} + +// ============================================================================ +// Embedding +// ============================================================================ + +export interface EmbedRequest { + imagePath: string; + extractAll?: boolean; +} + +export interface EmbedResponse { + success: boolean; + faces: FaceResult[]; + imagePath: string; + message?: string; +} + +export interface EmbedFromUrlRequest { + imageUrl: string; + extractAll?: boolean; +} + +export interface FaceEmbeddingResult { + bbox: BBox; + confidence: number; + embedding: number[]; +} + +export interface EmbedFromUrlResponse { + success: boolean; + faces: FaceEmbeddingResult[]; + message?: string; +} + +// ============================================================================ +// Clustering +// ============================================================================ + +export interface ClusterRequest { + inputDir: string; + minClusterSize?: number; + recursive?: boolean; + mergeThreshold?: number; +} + +export interface ClusterInfo { + clusterId: number; + faceCount: number; + photoCount: number; + photoPaths: string[]; +} + +export interface ClusterResponse { + success: boolean; + identityCount: number; + clusters: ClusterInfo[]; + noiseCount: number; + totalFaces: number; + totalPhotos: number; + multiPersonPhotos: string[]; + message?: string; +} + +// ============================================================================ +// Organization +// ============================================================================ + +export interface OrganizeRequest { + inputDir: string; + outputDir: string; + multiPersonStrategy?: MultiPersonStrategy; + noFaceStrategy?: NoFaceStrategy; + minClusterSize?: number; + recursive?: boolean; +} + +export interface OrganizeResponse { + success: boolean; + outputDir: string; + identityFolders: Record; + photosProcessed: number; + photosOrganized: number; + photosNoFace: number; + photosMultiPerson: number; + errors: string[]; + message?: string; +} + +// ============================================================================ +// Health +// ============================================================================ + +export interface HealthResponse { + status: HealthStatus; + version: string; + uptimeSeconds: number; +} + +export interface ReadinessResponse { + status: ReadinessStatus; + gpuAvailable: boolean; + modelLoaded: boolean; + redisConnected: boolean; + version: string; + uptimeSeconds: number; +} + +// ============================================================================ +// Identity CRUD +// ============================================================================ + +export interface CreateIdentityRequest { + name: string; + imagePaths: string[]; + minConfidence?: number; +} + +export interface UpdateIdentityRequest { + name?: string; + addImagePaths?: string[]; + addImageUrls?: string[]; +} + +export interface IdentityResponse { + id: string; + name: string; + imageCount: number; + sourcePaths: string[]; + createdAt: string; + updatedAt: string; + gender: string | null; +} + +export interface IdentityListResponse { + identities: IdentityResponse[]; + total: number; +} + +// ============================================================================ +// Identity Embedding (IP-Adapter integration) +// ============================================================================ + +export interface IdentityEmbeddingResponse { + identityId: string; + name: string; + embedding: number[]; + embeddingNorm: number; + imageCount: number; +} + +// ============================================================================ +// Identity Compare +// ============================================================================ + +export interface IdentityCompareRequest { + imageBase64: string; +} + +export interface IdentityCompareResponse { + identityId: string; + similarity: number; + confidence: ConfidenceLevel; + faceDetected: boolean; + message?: string; +} + +// ============================================================================ +// Search +// ============================================================================ + +export interface SearchRequest { + identityId: string; + searchPaths: string[]; + minSimilarity?: number; + recursive?: boolean; + limit?: number; +} + +export interface SearchMatch { + imagePath: string; + similarity: number; + confidence: ConfidenceLevel; +} + +export interface SearchResponse { + identityId: string; + matches: SearchMatch[]; + totalImagesSearched: number; +} + +export interface BatchSearchRequest { + identityIds: string[]; + searchPaths: string[]; + minSimilarity?: number; + recursive?: boolean; + limit?: number; +} + +export interface BatchSearchResponse { + results: SearchResponse[]; + totalImagesSearched: number; +} + +// ============================================================================ +// Namespaced Identity (media-gallery integration) +// ============================================================================ + +export interface BuildIdentityFromUrlsRequest { + namespace: string; + identityId: string; + displayName: string; + imageUrls: string[]; + minConfidence?: number; + upsert?: boolean; +} + +export interface IdentityBuildResponse { + success: boolean; + identityId: string; + namespace: string; + imageCount: number; + message?: string; +} + +export interface NamespacedSearchRequest { + namespace: string; + identityId: string; + imageUrl: string; + minSimilarity?: number; +} + +export interface NamespacedSearchResponse { + identityId: string; + namespace: string; + similarity: number; + confidence: ConfidenceLevel; + faceDetected: boolean; + message?: string; +} + +// ============================================================================ +// Gallery Scan +// ============================================================================ + +export interface ScanGalleryRequest { + threshold?: number; + limit?: number; + cursor?: string; + category?: string; +} + +export interface GalleryCandidate { + photoId: string; + similarity: number; + confidence: ConfidenceLevel; + thumbnailUrl: string; + originalUrl: string; + originalFilename: string; + faceQuality: number; +} + +export interface ScanGalleryResponse { + identityId: string; + candidates: GalleryCandidate[]; + totalScanned: number; + hasMore: boolean; + galleryTotal: number; + nextCursor?: string; +} diff --git a/services/imajin-identity/types/tsconfig.json b/services/imajin-identity/types/tsconfig.json new file mode 100644 index 00000000..88c55839 --- /dev/null +++ b/services/imajin-identity/types/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tooling/tsconfig/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/services/imajin-identity/types/tsup.config.ts b/services/imajin-identity/types/tsup.config.ts new file mode 100644 index 00000000..3bf41e04 --- /dev/null +++ b/services/imajin-identity/types/tsup.config.ts @@ -0,0 +1,3 @@ +import { createLibraryConfig } from '@lilith/configs/tsup/library'; + +export default createLibraryConfig();