Spaces:
Running
on
L40S
Running
on
L40S
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; | |
import { FiDownload } from 'react-icons/fi'; | |
import { PiImageSquare } from 'react-icons/pi'; | |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; | |
import { truncateFileName } from './lib/utils'; | |
import { useFaceLandmarkDetection } from './hooks/useFaceLandmarkDetection'; | |
import { About } from './components/About'; | |
import { Spinner } from './components/Spinner'; | |
import { useFacePokeAPI } from './hooks/useFacePokeAPI'; | |
import { Layout } from './layout'; | |
import { useMainStore } from './hooks/useMainStore'; | |
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 isGazingAtCursor = useMainStore(s => s.isGazingAtCursor); | |
const setIsGazingAtCursor = useMainStore(s => s.setIsGazingAtCursor); | |
const isFollowingCursor = useMainStore(s => s.isFollowingCursor); | |
const setIsFollowingCursor = useMainStore(s => s.setIsFollowingCursor); | |
const previewImage = useMainStore(s => s.previewImage); | |
const status = useMainStore(s => s.status); | |
const blendShapes = useMainStore(s => s.blendShapes); | |
const { | |
isDebugMode, | |
setIsDebugMode, | |
interruptMessage, | |
} = useFacePokeAPI() | |
const { | |
canvasRefCallback, | |
isMediaPipeReady, | |
handleMouseDown, | |
handleMouseUp, | |
handleMouseMove, | |
handleTouchStart, | |
handleTouchMove, | |
handleTouchEnd, | |
currentOpacity | |
} = useFaceLandmarkDetection() | |
// Refs | |
const videoRef = useRef<HTMLDivElement>(null); | |
// Handle file change | |
const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |
const files = event.target.files; | |
setImageFile(files?.[0] || undefined) | |
}, [setImageFile]); | |
const handleDownload = useCallback(() => { | |
if (previewImage) { | |
const link = document.createElement('a'); | |
link.href = previewImage; | |
link.download = 'result.webp'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
} | |
}, [previewImage]); | |
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-4 relative"> | |
<div className="flex flex-row items-center justify-between w-full"> | |
<div className="flex items-center space-x-2"> | |
<div className="flex items-center justify-center"> | |
<input | |
id="imageInput" | |
type="file" | |
accept="image/*" | |
onChange={handleFileChange} | |
className="hidden" | |
disabled={!isMediaPipeReady} | |
/> | |
<label | |
htmlFor="imageInput" | |
className={`cursor-pointer inline-flex items-center border border-transparent font-medium rounded-md text-white ${ | |
imageFile ? 'text-xs px-2 h-8' : 'text-lg px-4 h-12' | |
} ${ | |
isMediaPipeReady ? 'bg-slate-600 hover:bg-slate-500' : 'bg-slate-500 cursor-not-allowed' | |
} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 shadow-xl`} | |
> | |
<PiImageSquare className="w-4 h-4 mr-1.5" /> | |
{imageFile ? `Replace` : (isMediaPipeReady ? 'Choose a portrait photo' : 'Initializing...')} | |
</label> | |
</div> | |
{previewImage && ( | |
<button | |
onClick={handleDownload} | |
className="inline-flex items-center px-2 h-8 border border-transparent text-xs font-medium rounded-md text-white bg-zinc-600 hover:bg-zinc-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-zinc-500 shadow-xl" | |
> | |
<FiDownload className="w-4 h-4 mr-1.5" /> | |
Download | |
</button> | |
)} | |
</div> | |
{previewImage && <div className="flex items-center space-x-2"> | |
{/* experimental features, not active yet */} | |
{/* | |
<label className="mt-4 flex items-center"> | |
<input | |
type="checkbox" | |
checked={isGazingAtCursor} | |
onChange={(e) => setIsGazingAtCursor(!isGazingAtCursor)} | |
className="mr-2" | |
/> | |
Autotrack eyes | |
</label> | |
<label className="mt-4 flex items-center"> | |
<input | |
type="checkbox" | |
checked={isFollowingCursor} | |
onChange={(e) => setIsFollowingCursor(!isFollowingCursor)} | |
className="mr-2" | |
/> | |
Autotrack head | |
</label> | |
*/} | |
<label className="mt-2 flex items-center text-sm mr-2"> | |
<input | |
type="checkbox" | |
checked={isDebugMode} | |
onChange={(e) => setIsDebugMode(e.target.checked)} | |
className="mr-2" | |
/> | |
Show face markers | |
</label> | |
</div>} | |
</div> | |
{previewImage && ( | |
<div className="mt-2 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" | |
onMouseDown={handleMouseDown} | |
onMouseUp={handleMouseUp} | |
onMouseMove={handleMouseMove} | |
onTouchStart={(e) => { | |
e.preventDefault(); // Prevent default touch behavior on canvas | |
handleTouchStart(e); | |
}} | |
onTouchMove={(e) => { | |
e.preventDefault(); // Prevent default touch behavior on canvas | |
handleTouchMove(e); | |
}} | |
onTouchEnd={(e) => { | |
e.preventDefault(); // Prevent default touch behavior on canvas | |
handleTouchEnd(e); | |
}} | |
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> | |
<About /> | |
</Layout> | |
); | |
} | |