diff --git a/features/video-studio/frontend-demo/src/App.tsx b/features/video-studio/frontend-demo/src/App.tsx index 41fe545ea..717421f47 100644 --- a/features/video-studio/frontend-demo/src/App.tsx +++ b/features/video-studio/frontend-demo/src/App.tsx @@ -25,34 +25,46 @@ export function App(): ReactElement { const [identityFrameCounts, setIdentityFrameCounts] = useState>(new Map()); // Called when 📷 is clicked on a face box. - // Creates a new identity from the portrait, or updates an existing one's portrait. - // Returns the identity ID so the wrapper can auto-assign the face. + // Creates a new identity (portrait optional — null if capture failed), or + // updates an existing one's portrait. Returns the identity ID for auto-assignment. const handleCapturePortrait = useCallback( ( _faceIndex: number, - portraitDataUrl: string, + portraitDataUrl: string | null, existingIdentityId: string | null, ): string => { if (existingIdentityId) { - // Update portrait of existing identity. - setIdentities((prev) => - prev.map((id) => - id.id === existingIdentityId ? { ...id, thumbnailUrl: portraitDataUrl } : id, - ), - ); + // Update portrait of existing identity if we have one. + if (portraitDataUrl) { + setIdentities((prev) => + prev.map((id) => + id.id === existingIdentityId ? { ...id, thumbnailUrl: portraitDataUrl } : id, + ), + ); + } return existingIdentityId; } // Create new identity. const id = generateId(); const name = `Person ${nextPersonNum++}`; - setIdentities((prev) => [...prev, { id, name, thumbnailUrl: portraitDataUrl }]); + setIdentities((prev) => [ + ...prev, + { id, name, ...(portraitDataUrl ? { thumbnailUrl: portraitDataUrl } : {}) }, + ]); setIdentityModes((prev) => new Map([...prev, [id, 'mask' as DisguiseMode]])); return id; }, [], ); + const handleAddIdentity = useCallback(() => { + const id = generateId(); + const name = `Person ${nextPersonNum++}`; + setIdentities((prev) => [...prev, { id, name }]); + setIdentityModes((prev) => new Map([...prev, [id, 'mask' as DisguiseMode]])); + }, []); + const handleDeleteIdentity = useCallback((id: string) => { setIdentities((prev) => prev.filter((i) => i.id !== id)); setIdentityModes((prev) => { const n = new Map(prev); n.delete(id); return n; }); @@ -130,6 +142,7 @@ export function App(): ReactElement { identities={identities} identityModes={identityModes} identityFrameCounts={identityFrameCounts} + onAdd={handleAddIdentity} onDelete={handleDeleteIdentity} onModeChange={handleIdentityModeChange} onRename={handleRename} diff --git a/features/video-studio/frontend-demo/src/components/DisguiseParamsPanel.tsx b/features/video-studio/frontend-demo/src/components/DisguiseParamsPanel.tsx index 712dadde3..73a405a86 100644 --- a/features/video-studio/frontend-demo/src/components/DisguiseParamsPanel.tsx +++ b/features/video-studio/frontend-demo/src/components/DisguiseParamsPanel.tsx @@ -124,7 +124,7 @@ export function DisguiseParamsPanel({ Succubus params onSuccubusChange(resolveSuccubusParams())} /> - set('hornHeight', v)} /> + set('hornHeight', v)} /> set('hornSweep', v)} /> set('eyeGlow', v)} /> String(v)} onChange={(v) => set('sparkleCount', Math.round(v))} /> diff --git a/features/video-studio/frontend-demo/src/components/FileVideoView.tsx b/features/video-studio/frontend-demo/src/components/FileVideoView.tsx index f2a1a8575..6fab5dbd9 100644 --- a/features/video-studio/frontend-demo/src/components/FileVideoView.tsx +++ b/features/video-studio/frontend-demo/src/components/FileVideoView.tsx @@ -17,7 +17,7 @@ const MAX_HEIGHT = 720; export interface FileVideoViewProps { identities: FaceIdentity[]; resolveIdentityMode: (identityId: string) => DisguiseMode | undefined; - onCapturePortrait: (faceIndex: number, portraitDataUrl: string, existingIdentityId: string | null) => string | null; + onCapturePortrait: (faceIndex: number, portraitDataUrl: string | null, existingIdentityId: string | null) => string | null; onFrameAccounting: (faceIdentities: ReadonlyMap) => void; } diff --git a/features/video-studio/frontend-demo/src/components/IdentityPanel.tsx b/features/video-studio/frontend-demo/src/components/IdentityPanel.tsx index 62a304ce2..8083f7460 100644 --- a/features/video-studio/frontend-demo/src/components/IdentityPanel.tsx +++ b/features/video-studio/frontend-demo/src/components/IdentityPanel.tsx @@ -11,6 +11,7 @@ export interface IdentityPanelProps { identityModes: ReadonlyMap; /** Frame counts per identity ID — drives the donut chart. */ identityFrameCounts: ReadonlyMap; + onAdd: () => void; onDelete: (id: string) => void; onModeChange: (id: string, mode: DisguiseMode) => void; onRename: (id: string, name: string) => void; @@ -130,6 +131,7 @@ export function IdentityPanel({ identities, identityModes, identityFrameCounts, + onAdd, onDelete, onModeChange, onRename, @@ -162,15 +164,15 @@ export function IdentityPanel({
Identities - {identities.length > 0 && ( - 📷 click face to capture - )} +
{identities.length === 0 ? (

No identities yet.
- Click 📷 on a detected face to capture a portrait and create one. + Use + Add or click 📷 on a detected face.

) : (
@@ -263,10 +265,15 @@ const styles = { letterSpacing: '0.08em', color: '#555', }, - hint: { - fontSize: '10px', - color: '#444', - fontStyle: 'italic' as const, + addBtn: { + fontSize: '11px', + padding: '2px 8px', + background: '#1e0f2e', + color: '#9b59b6', + border: '1px solid #3a1a5a', + borderRadius: '4px', + cursor: 'pointer', + fontWeight: 600, }, empty: { margin: 0, diff --git a/features/video-studio/frontend-demo/src/components/LiveCameraView.tsx b/features/video-studio/frontend-demo/src/components/LiveCameraView.tsx index b675fdcfb..5d3b22143 100644 --- a/features/video-studio/frontend-demo/src/components/LiveCameraView.tsx +++ b/features/video-studio/frontend-demo/src/components/LiveCameraView.tsx @@ -10,7 +10,7 @@ import type { FaceIdentity } from '@vs-live/components/FaceSelectionOverlay'; export interface LiveCameraViewProps { identities: FaceIdentity[]; resolveIdentityMode: (identityId: string) => DisguiseMode | undefined; - onCapturePortrait: (faceIndex: number, portraitDataUrl: string, existingIdentityId: string | null) => string | null; + onCapturePortrait: (faceIndex: number, portraitDataUrl: string | null, existingIdentityId: string | null) => string | null; onFrameAccounting: (faceIdentities: ReadonlyMap) => void; } diff --git a/features/video-studio/frontend-live/src/components/DisguiseVideoWithFaceSelector.tsx b/features/video-studio/frontend-live/src/components/DisguiseVideoWithFaceSelector.tsx index 898d3916b..3241a46be 100644 --- a/features/video-studio/frontend-live/src/components/DisguiseVideoWithFaceSelector.tsx +++ b/features/video-studio/frontend-live/src/components/DisguiseVideoWithFaceSelector.tsx @@ -54,15 +54,16 @@ export interface DisguiseVideoWithFaceSelectorProps resolveIdentityMode?: (identityId: string) => DisguiseMode | undefined; /** * Called when the camera button is clicked on a face box. - * Receives: face index, JPEG data URL of the cropped portrait, and the - * face's current identity ID (null = unassigned). + * Receives: face index, JPEG data URL of the cropped portrait (null if capture + * failed — e.g., face not yet in detection results), and the face's current + * identity ID (null = unassigned). * Return the identity ID to assign to this face (existing or newly created), * or null to skip the assignment. * Synchronous — create the identity in the parent and return its ID. */ onCapturePortrait?: ( faceIndex: number, - portraitDataUrl: string, + portraitDataUrl: string | null, existingIdentityId: string | null, ) => string | null; /** @@ -210,9 +211,7 @@ export function DisguiseVideoWithFaceSelector({ }, []); const handlePortraitCapture = useCallback((faceIndex: number) => { - const dataUrl = portraitCaptureRef.current?.(faceIndex); - if (!dataUrl) return; - + const dataUrl = portraitCaptureRef.current?.(faceIndex) ?? null; const existingIdentityId = faceIdentitiesRef.current.get(faceIndex) ?? null; const resultId = onCapturePortraitRef.current?.(faceIndex, dataUrl, existingIdentityId); @@ -276,20 +275,22 @@ export function DisguiseVideoWithFaceSelector({ faceDisguiseModes={faceDisguiseModes} portraitCaptureRef={portraitCaptureRef} /> - + {showOverlay && ( + + )}
); } diff --git a/features/video-studio/frontend-live/src/renderers/params.ts b/features/video-studio/frontend-live/src/renderers/params.ts index a8a0b1361..6d8c37f8d 100644 --- a/features/video-studio/frontend-live/src/renderers/params.ts +++ b/features/video-studio/frontend-live/src/renderers/params.ts @@ -21,7 +21,7 @@ export interface DemonParams { } export interface SuccubusParams { - /** Horn height multiplier — 0.4–2.0. */ + /** Horn height multiplier — 0.4–4.0. */ hornHeight: number; /** Horn tip X-sweep multiplier — 0.4–2.0. */ hornSweep: number; @@ -57,14 +57,14 @@ export const DEFAULT_DEMON_PARAMS: DemonParams = { }; export const DEFAULT_SUCCUBUS_PARAMS: SuccubusParams = { - hornHeight: 2.00, - hornSweep: 1.95, - eyeGlow: 1.70, - sparkleCount: 7, + hornHeight: 1.00, + hornSweep: 1.00, + eyeGlow: 1.00, + sparkleCount: 3, sparkleSize: 0.85, wingSpread: 0.80, - earHeight: 2.00, - tintStrength: 1.45, + earHeight: 1.00, + tintStrength: 1.00, tongue: true, };