File size: 6,806 Bytes
921d377 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | /**
* IdentityTools - Optional identity-aware edit buttons for Edit Studio.
*
* Additive + non-destructive:
* - Only rendered when avatar identity models are installed
* - Does NOT replace or modify existing Quick Enhance / Background tools
* - Controlled by parent via `show` / capability booleans
*
* Basic Pack (InsightFace + InstantID):
* - Fix Faces+ (identity-aware face restoration)
* - Inpaint (Preserve Person)
* - Change BG (Preserve Person)
*
* Full Pack (+ InSwapper):
* - Face Swap (NOW FUNCTIONAL — uses ReActor/InSwapper workflow)
*/
import React, { useState } from 'react';
import { Loader2, UserCheck, Scan, ImageOff, Repeat, Shield } from 'lucide-react';
import { applyIdentityTool, IDENTITY_TOOLS, } from '../enhance/identityApi';
import { FaceSwapRefInput } from './FaceSwapRefInput';
// ---------------------------------------------------------------------------
// Icon map
// ---------------------------------------------------------------------------
function getIcon(id) {
switch (id) {
case 'fix_faces_identity':
return <UserCheck size={18} className="text-cyan-400"/>;
case 'inpaint_identity':
return <Scan size={18} className="text-cyan-400"/>;
case 'change_bg_identity':
return <ImageOff size={18} className="text-cyan-400"/>;
case 'face_swap':
return <Repeat size={18} className="text-cyan-400"/>;
default:
return <Shield size={18} className="text-cyan-400"/>;
}
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function IdentityTools({ backendUrl, apiKey, imageUrl, onResult, onError, disabled = false, hasBasicIdentity, hasFaceSwap, maskDataUrl, }) {
const [loading, setLoading] = useState(null);
// Face Swap reference input state (additive)
const [showFaceSwapRef, setShowFaceSwapRef] = useState(false);
const [faceSwapRefUrl, setFaceSwapRefUrl] = useState(null);
// Don't render at all if no identity models installed
if (!hasBasicIdentity)
return null;
const anyLoading = loading !== null;
const isDisabled = disabled || !imageUrl;
const handleTool = async (toolType, referenceUrl) => {
if (!imageUrl || anyLoading || isDisabled)
return;
// For face_swap, require a reference image — show picker if not set
if (toolType === 'face_swap' && !referenceUrl && !faceSwapRefUrl) {
setShowFaceSwapRef(true);
return;
}
const effectiveRefUrl = referenceUrl || faceSwapRefUrl;
setLoading(toolType);
try {
const result = await applyIdentityTool({
backendUrl,
apiKey,
imageUrl,
toolType,
referenceImageUrl: toolType === 'face_swap' ? effectiveRefUrl || undefined : undefined,
maskDataUrl: toolType === 'inpaint_identity' && maskDataUrl ? maskDataUrl : undefined,
});
const resultUrl = result?.media?.images?.[0];
if (resultUrl) {
onResult(resultUrl, toolType);
// Reset face swap state after successful swap
if (toolType === 'face_swap') {
setFaceSwapRefUrl(null);
setShowFaceSwapRef(false);
}
}
else {
onError('Identity tool completed but no image was returned.');
}
}
catch (e) {
onError(e instanceof Error ? e.message : 'Identity tool failed');
}
finally {
setLoading(null);
}
};
// Filter visible tools based on installed models
const visibleTools = IDENTITY_TOOLS.filter((tool) => {
if (tool.pack === 'full')
return hasFaceSwap;
return true; // basic pack tools always visible when hasBasicIdentity
});
return (<div className="space-y-3">
{/* Section header */}
<div className="text-xs uppercase tracking-wider text-white/40 font-semibold flex items-center gap-2">
<Shield size={14}/>
Identity Tools
<span className="text-[8px] px-1.5 py-0.5 rounded-full bg-cyan-500/15 border border-cyan-500/20 text-cyan-300/60 font-medium normal-case tracking-normal">
Beta
</span>
</div>
<p className="text-[10px] text-white/25 leading-relaxed -mt-1">
Edit while preserving facial identity. Uses installed Avatar & Identity models.
</p>
{/* Tool buttons */}
<div className="grid grid-cols-1 gap-2">
{visibleTools.map((tool) => (<div key={tool.id}>
<button onClick={() => handleTool(tool.id)} disabled={isDisabled || anyLoading} className={`
w-full flex items-center gap-3 p-3 rounded-xl border transition-all text-left
${loading === tool.id
? 'bg-cyan-500/20 border-cyan-500/40 text-cyan-300'
: isDisabled || anyLoading
? 'bg-white/5 border-white/5 text-white/30 cursor-not-allowed'
: 'bg-white/5 border-white/10 text-white/80 hover:bg-cyan-500/10 hover:border-cyan-500/30 hover:text-cyan-200'}
`}>
<div className={`
w-9 h-9 rounded-lg flex items-center justify-center
${loading === tool.id ? 'bg-cyan-500/30' : 'bg-white/10'}
`}>
{loading === tool.id ? (<Loader2 size={18} className="animate-spin text-cyan-400"/>) : (getIcon(tool.id))}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium flex items-center gap-2">
{tool.label}
<span className="text-[9px] px-1.5 py-0.5 rounded bg-cyan-500/10 text-cyan-300/40">
{tool.pack === 'full' ? 'Full Pack' : 'Identity'}
</span>
</div>
<div className="text-[10px] text-white/40 truncate">
{tool.description}
</div>
</div>
</button>
{/* Face Swap reference input — shown inline below the Face Swap button */}
{tool.id === 'face_swap' && showFaceSwapRef && (<FaceSwapRefInput backendUrl={backendUrl} apiKey={apiKey} onReferenceReady={(url) => {
setFaceSwapRefUrl(url);
setShowFaceSwapRef(false);
// Auto-trigger the swap immediately
handleTool('face_swap', url);
}} onCancel={() => setShowFaceSwapRef(false)}/>)}
</div>))}
</div>
</div>);
}
|