Spaces:
Running
on
L40S
Running
on
L40S
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; | |
import { RotateCcw } from 'lucide-react'; | |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; | |
import { truncateFileName } from './lib/utils'; | |
import { useFaceLandmarkDetection } from './hooks/useFaceLandmarkDetection'; | |
import { PoweredBy } from './components/PoweredBy'; | |
import { Spinner } from './components/Spinner'; | |
import { DoubleCard } from './components/DoubleCard'; | |
import { useFacePokeAPI } from './hooks/useFacePokeAPI'; | |
import { Layout } from './layout'; | |
import { useMainStore } from './hooks/useMainStore'; | |
import { convertImageToBase64 } from './lib/convertImageToBase64'; | |
export function App() { | |
const error = useMainStore(s => s.error); | |
const setError = useMainStore(s => s.setError); | |
const imageFile = useMainStore(s => s.imageFile); | |
const setImageFile = useMainStore(s => s.setImageFile); | |
const originalImage = useMainStore(s => s.originalImage); | |
const setOriginalImage = useMainStore(s => s.setOriginalImage); | |
const previewImage = useMainStore(s => s.previewImage); | |
const setPreviewImage = useMainStore(s => s.setPreviewImage); | |
const resetImage = useMainStore(s => s.resetImage); | |
const { | |
status, | |
setStatus, | |
isDebugMode, | |
setIsDebugMode, | |
interruptMessage, | |
} = useFacePokeAPI() | |
// State for face detection | |
const { | |
canvasRef, | |
canvasRefCallback, | |
mediaPipeRef, | |
faceLandmarks, | |
isMediaPipeReady, | |
blendShapes, | |
setFaceLandmarks, | |
setBlendShapes, | |
handleMouseDown, | |
handleMouseUp, | |
handleMouseMove, | |
handleMouseEnter, | |
handleMouseLeave, | |
currentOpacity | |
} = useFaceLandmarkDetection() | |
// Refs | |
const videoRef = useRef<HTMLDivElement>(null); | |
// Handle file change | |
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => { | |
const files = event.target.files; | |
if (files && files[0]) { | |
setImageFile(files[0]); | |
setStatus(`File selected: ${truncateFileName(files[0].name, 16)}`); | |
try { | |
const image = await convertImageToBase64(files[0]); | |
setPreviewImage(image); | |
setOriginalImage(image); | |
} catch (err) { | |
console.log(`failed to convert the image: `, err); | |
setImageFile(null); | |
setStatus(''); | |
setPreviewImage(''); | |
setOriginalImage(''); | |
setFaceLandmarks([]); | |
setBlendShapes([]); | |
} | |
} else { | |
setImageFile(null); | |
setStatus(''); | |
setPreviewImage(''); | |
setOriginalImage(''); | |
setFaceLandmarks([]); | |
setBlendShapes([]); | |
} | |
}, [isMediaPipeReady, setImageFile, setPreviewImage, setOriginalImage, setFaceLandmarks, setBlendShapes, setStatus]); | |
const canDisplayBlendShapes = false | |
// Display blend shapes | |
const displayBlendShapes = useMemo(() => ( | |
<div className="mt-4"> | |
<h3 className="text-lg font-semibold mb-2">Blend Shapes</h3> | |
<ul className="space-y-1"> | |
{(blendShapes?.[0]?.categories || []).map((shape, index) => ( | |
<li key={index} className="flex items-center"> | |
<span className="w-32 text-sm">{shape.categoryName || shape.displayName}</span> | |
<div className="w-full bg-gray-200 rounded-full h-2.5"> | |
<div | |
className="bg-blue-600 h-2.5 rounded-full" | |
style={{ width: `${shape.score * 100}%` }} | |
></div> | |
</div> | |
<span className="ml-2 text-sm">{shape.score.toFixed(2)}</span> | |
</li> | |
))} | |
</ul> | |
</div> | |
), [JSON.stringify(blendShapes)]) | |
// JSX | |
return ( | |
<Layout> | |
{error && ( | |
<Alert variant="destructive"> | |
<AlertTitle>Error</AlertTitle> | |
<AlertDescription>{error}</AlertDescription> | |
</Alert> | |
)} | |
{interruptMessage && ( | |
<Alert> | |
<AlertTitle>Notice</AlertTitle> | |
<AlertDescription>{interruptMessage}</AlertDescription> | |
</Alert> | |
)} | |
<div className="mb-5 relative"> | |
<div className="flex flex-row items-center justify-between w-full"> | |
<div className="relative"> | |
<input | |
id="imageInput" | |
type="file" | |
accept="image/*" | |
onChange={handleFileChange} | |
className="hidden" | |
disabled={!isMediaPipeReady} | |
/> | |
<label | |
htmlFor="imageInput" | |
className={`cursor-pointer inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md text-white ${ | |
isMediaPipeReady ? 'bg-gray-600 hover:bg-gray-500' : 'bg-gray-500 cursor-not-allowed' | |
} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 shadow-xl`} | |
> | |
<Spinner /> | |
{imageFile ? truncateFileName(imageFile.name, 32) : (isMediaPipeReady ? 'Choose an image' : 'Initializing...')} | |
</label> | |
</div> | |
{previewImage && <label className="mt-4 flex items-center"> | |
<input | |
type="checkbox" | |
checked={isDebugMode} | |
onChange={(e) => setIsDebugMode(e.target.checked)} | |
className="mr-2" | |
/> | |
Show face landmarks on hover | |
</label>} | |
</div> | |
{previewImage && ( | |
<div className="mt-5 relative shadow-2xl rounded-xl overflow-hidden"> | |
<img | |
src={previewImage} | |
alt="Preview" | |
className="w-full" | |
/> | |
<canvas | |
ref={canvasRefCallback} | |
className="absolute top-0 left-0 w-full h-full select-none" | |
onMouseEnter={handleMouseEnter} | |
onMouseLeave={handleMouseLeave} | |
onMouseDown={handleMouseDown} | |
onMouseUp={handleMouseUp} | |
onMouseMove={handleMouseMove} | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: '100%', | |
height: '100%', | |
opacity: isDebugMode ? currentOpacity : 0.0, | |
transition: 'opacity 0.2s ease-in-out' | |
}} | |
/> | |
</div> | |
)} | |
{canDisplayBlendShapes && displayBlendShapes} | |
</div> | |
<PoweredBy /> | |
</Layout> | |
); | |
} | |