Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
VerifyResult,
IdentifyInput,
IdentifyResult,
CrossCollectionSearchInput,
CrossCollectionSearchResult,
CompareInput,
LivenessInput,
LivenessResult,
Expand Down Expand Up @@ -408,6 +410,16 @@ export class FacesResource {
);
}

/** Search for matching faces across multiple or all collections. */
async search(input: CrossCollectionSearchInput): Promise<CrossCollectionSearchResult> {
const form = new FormData();
form.append("image", await toBlob(input.image), input.filename ?? "image.jpg");
if (input.collectionIds?.length) form.append("collection_ids", input.collectionIds.join(","));
if (input.topK !== undefined) form.append("top_k", String(input.topK));
if (input.threshold !== undefined) form.append("threshold", String(input.threshold));
return this.client._request<CrossCollectionSearchResult>("POST", "/search", { formData: form });
}

/**
* Liveness detection — check whether the face in the image is live (anti-spoofing).
*/
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type {
IdentifyMatch,
IdentifyInput,
IdentifyResult,
CrossCollectionSearchInput,
CrossCollectionSearchMatch,
CrossCollectionSearchResult,
SkippedCollection,
DetectedFace,
CompareInput,
LivenessInput,
Expand Down
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@ export interface IdentifyResult {
imageSize?: ImageSize
}

export interface CrossCollectionSearchInput {
image: Blob | Buffer | ArrayBuffer
collectionIds?: string[]
topK?: number
threshold?: number
filename?: string
}

export interface CrossCollectionSearchMatch extends IdentifyMatch {
collectionId: string
}

export interface SkippedCollection {
id: string
name: string
reason: string
}

export interface CrossCollectionSearchResult {
matches: CrossCollectionSearchMatch[]
queryTimeMs: number
collectionsSearched: number
skippedCollections: SkippedCollection[]
detectedFaces?: DetectedFace[]
imageSize?: ImageSize
}

export interface CompareInput {
image1: Blob | Buffer | ArrayBuffer
image2: Blob | Buffer | ArrayBuffer
Expand Down
1 change: 1 addition & 0 deletions test/contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const cases = {
delete: (f) => f.delete(col, 'face_1'),
verify: (f) => f.verify(col, { image: img(), faceId: 'face_1', threshold: 0.5 }),
identify: (f) => f.identify(col, { image: img(), topK: 3, threshold: 0.5 }),
search: (f) => f.search({ image: img(), collectionIds: ['col_1', 'col_2'], topK: 3, threshold: 0.5 }),
liveness: (f) => f.liveness(col, { image: img() }),
activeLiveness: (f) => f.activeLiveness(col, Array.from({ length: 5 }, img)),
createLivenessSession: (f) => f.createLivenessSession(col),
Expand Down
11 changes: 11 additions & 0 deletions test/faces.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ test('activeLiveness surfaces IMAGE_REQUIRED', async () => {

const face = { id: 'f1', collectionId: 'col_1', externalId: 'u1', metadata: {}, createdAt: 'x' }

test('search parses skipped collections and surfaces typed profile mismatch', async () => {
stubFetch(200, { success: true, data: { matches: [], queryTimeMs: 8, collectionsSearched: 1, skippedCollections: [{ id: 'c2', name: 'Legacy', reason: 'embedding_profile_mismatch' }] } })
const result = await client.faces.search({ image: img(), collectionIds: ['c1', 'c2'], topK: 3 })
assert.equal(calls[0].url, 'http://api.test/api/v1/search')
assert.equal(calls[0].init.body.get('collection_ids'), 'c1,c2')
assert.equal(result.skippedCollections[0].reason, 'embedding_profile_mismatch')

stubFetch(409, { success: false, error: { code: 'EMBEDDING_PROFILE_MISMATCH', message: 'no compatible collections' } })
await assert.rejects(client.faces.search({ image: img() }), (err) => err instanceof LiveXFaceApiError && err.statusCode === 409 && err.code === 'EMBEDDING_PROFILE_MISMATCH')
})

test('register sends liveness_token when given', async () => {
stubFetch(201, { success: true, data: face })
await client.faces.register('col_1', { externalId: 'u1', image: img(), livenessToken: 'lvt_abc' })
Expand Down
Loading