extractEmbeddings integration
This commit is contained in:
@@ -2,4 +2,6 @@ node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
**/generated/prisma
|
||||
/generated/prisma
|
||||
|
||||
/generated/prisma
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
});
|
||||
@@ -8,4 +8,6 @@ export const componentLoader = new ComponentLoader()
|
||||
|
||||
export const Components = {
|
||||
Dashboard: componentLoader.add('Dashboard', path.join(__dirname, './components/Dashboard.tsx'), 'components'),
|
||||
UploadFace: componentLoader.add('UploadFace', path.join(__dirname, './components/UploadFace.tsx'), 'components'),
|
||||
FaceEmbeddingField: componentLoader.add('FaceEmbeddingField', path.join(__dirname, './components/FaceEmbeddingField.tsx'), 'components'),
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { BasePropertyProps } from 'adminjs'
|
||||
import { Label } from '@adminjs/design-system'
|
||||
|
||||
const FaceEmbeddingField: React.FC<BasePropertyProps> = ({ record, property }) => {
|
||||
const has = !!record?.params?.faceEmbedding
|
||||
return (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Label>{property.label}</Label>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
padding: '3px 10px',
|
||||
borderRadius: '4px',
|
||||
background: has ? '#dcfce7' : '#fee2e2',
|
||||
color: has ? '#16a34a' : '#dc2626',
|
||||
}}>
|
||||
{has ? 'SET' : 'UNSET'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FaceEmbeddingField
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { Box, H3, Text, Button, MessageBox } from '@adminjs/design-system'
|
||||
import { ActionProps, useRecord } from 'adminjs'
|
||||
|
||||
const UploadFace: React.FC<ActionProps> = ({ record, action }) => {
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [base64, setBase64] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [notice, setNotice] = useState<{ message: string, type: 'success' | 'error' } | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string
|
||||
setPreview(result)
|
||||
setBase64(result.split(',')[1]) // strip data:image/jpeg;base64,
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!base64) return
|
||||
setLoading(true)
|
||||
setNotice(null)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/admin/api/resources/User/records/${record?.params.id}/uploadFace`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ base64 }),
|
||||
}
|
||||
)
|
||||
const data = await res.json()
|
||||
setNotice(data.notice)
|
||||
} catch (e: any) {
|
||||
setNotice({ message: e.message, type: 'error' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box padding="xl">
|
||||
<H3 marginBottom="md">Upload face image</H3>
|
||||
<Text color="grey60" marginBottom="lg">
|
||||
Upload a JPEG photo of the user to generate and store their face embedding.
|
||||
</Text>
|
||||
|
||||
{notice && (
|
||||
<MessageBox
|
||||
message={notice.message}
|
||||
variant={notice.type === 'success' ? 'success' : 'danger'}
|
||||
marginBottom="lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box marginBottom="lg">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/jpeg"
|
||||
onChange={handleFile}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Button onClick={() => inputRef.current?.click()} variant="outlined">
|
||||
Choose JPEG
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{preview && (
|
||||
<Box marginBottom="lg">
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
style={{ width: 160, height: 160, objectFit: 'cover', borderRadius: 8, border: '1px solid #e5e7eb' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!base64 || loading}
|
||||
variant="contained"
|
||||
>
|
||||
{loading ? 'Processing…' : 'Save embedding'}
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadFace
|
||||
@@ -1,5 +1,53 @@
|
||||
import { prisma } from "../../../lib/prisma.js";
|
||||
import { getModelByName } from "@adminjs/prisma";
|
||||
import { spawn } from "child_process";
|
||||
import { Components } from '../components.js'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'path'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const execPath = '../../../../bin';
|
||||
const binaryPath = path.join(__dirname, execPath, 'extractEmbeddings')
|
||||
|
||||
async function extractEmbedding(base64: string): Promise<string> {
|
||||
console.log(base64);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(binaryPath, [], {
|
||||
cwd: path.join(__dirname, execPath)
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
proc.stdout.on('data', (d) => stdout += d)
|
||||
proc.stderr.on('data', (d) => stderr += d)
|
||||
|
||||
proc.stdin.on('error', (err) => {
|
||||
// ignore EPIPE here, we'll catch it in close handler
|
||||
})
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new Error(`Failed to start binary: ${err.message}`))
|
||||
})
|
||||
|
||||
proc.on('close', (code) => {
|
||||
console.log('extractEmbeddings exited with code', code)
|
||||
console.log('stdout:', stdout)
|
||||
console.log('stderr:', stderr)
|
||||
if (code !== 0) return reject(new Error(stderr || `exited with code ${code}`))
|
||||
try {
|
||||
const result = JSON.parse(stdout)
|
||||
if (!result.success) return reject(new Error(result.error))
|
||||
resolve(JSON.stringify(result.descriptor))
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse output: ${stdout}`))
|
||||
}
|
||||
})
|
||||
|
||||
proc.stdin.write(base64)
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
resource: {
|
||||
@@ -10,14 +58,56 @@ export default {
|
||||
titleProperty: "id",
|
||||
navigation: { icon: "User" },
|
||||
listProperties: ["id", "firstName", "lastName"],
|
||||
showProperties: ["id", "firstName", "lastName"],
|
||||
showProperties: ["id", "firstName", "lastName", "faceEmbedding"],
|
||||
editProperties: ["firstName", "lastName"],
|
||||
filterProperties: ["firstName", "lastName"],
|
||||
properties: {
|
||||
faceEmbedding: {
|
||||
isVisible: false,
|
||||
type: "string"
|
||||
isVisible: { list: false, show: true, edit: false, filter: false },
|
||||
type: "string",
|
||||
components: {
|
||||
list: Components.FaceEmbeddingField,
|
||||
show: Components.FaceEmbeddingField,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
uploadFace: {
|
||||
actionType: 'record',
|
||||
icon: 'Camera',
|
||||
label: 'Upload face',
|
||||
showInDrawer: true,
|
||||
handler: async (request: any, response: any, context: any) => {
|
||||
const { record, currentAdmin } = context
|
||||
if (request.method === 'post') {
|
||||
const { base64 } = request.payload
|
||||
if (!base64) {
|
||||
return {
|
||||
record: record.toJSON(currentAdmin),
|
||||
notice: { message: 'No image provided', type: 'error' },
|
||||
}
|
||||
}
|
||||
try {
|
||||
const embedding = await extractEmbedding(base64)
|
||||
await prisma.user.update({
|
||||
where: { id: record.params.id },
|
||||
data: { faceEmbedding: embedding },
|
||||
})
|
||||
return {
|
||||
record: record.toJSON(currentAdmin),
|
||||
notice: { message: 'Face embedding saved!', type: 'success' },
|
||||
}
|
||||
} catch (e: any) {
|
||||
return {
|
||||
record: record.toJSON(currentAdmin),
|
||||
notice: { message: `Failed: ${e.message}`, type: 'error' },
|
||||
}
|
||||
}
|
||||
}
|
||||
return { record: record.toJSON(currentAdmin) }
|
||||
},
|
||||
component: Components.UploadFace,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user