Company2 min read
Internal Productivity & Biometric Suite
Client-side media processing plus a face verification system for Kiosk terminals.
- Next.js
- Capacitor
- FastAPI
- Qdrant
- FFmpeg
- IndexedDB
An internal suite in two parts: in-browser media processing, and a face verification system running on Kiosk terminals.
Client-side media processing#
The requirement: users upload short videos that must be compressed before sending. Compressing server-side would mean pushing the full-size original over the weak connections found at points of sale.
The answer was ffmpeg.wasm running in the browser, inside a Web Worker so the UI stays
responsive:
self.onmessage = async ({ data }: MessageEvent<TranscodeRequest>) => {
const ffmpeg = await loadFFmpeg();
await ffmpeg.writeFile('in.mp4', data.bytes);
await ffmpeg.exec([
'-i', 'in.mp4',
/* CRF 28 + a fast preset: small enough to upload, clear enough to review. */
'-vcodec', 'libx264', '-crf', '28', '-preset', 'veryfast',
'-vf', 'scale=-2:720',
'out.mp4',
]);
const output = await ffmpeg.readFile('out.mp4');
/* Transferable: hand over the buffer instead of copying it to the main thread. */
self.postMessage({ bytes: output }, [(output as Uint8Array).buffer]);
};Face verification#
The biometric half matches faces via embedding vectors stored in Qdrant. Source images are never retained:
@router.post("/verify")
async def verify(payload: VerifyRequest) -> VerifyResponse:
embedding = encoder.encode(payload.frame)
hits = qdrant.search(
collection_name="faces",
query_vector=embedding,
limit=1,
)
# Threshold set high: a false reject is an inconvenience, a false accept is not.
if not hits or hits[0].score < MATCH_THRESHOLD:
return VerifyResponse(matched=False)
return VerifyResponse(matched=True, subject_id=hits[0].payload["subject_id"])Outcome#
- Video compressed on the user's own device, cutting upload size dramatically
- UI stays responsive throughout, thanks to the Worker
- The verification system never stores raw face images