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 &amp; 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>);
}