diff --git a/EXPRESSION_SYSTEM.md b/EXPRESSION_SYSTEM.md new file mode 100644 index 0000000..dc7adb2 --- /dev/null +++ b/EXPRESSION_SYSTEM.md @@ -0,0 +1,290 @@ +# Expression System Documentation + +## Overview + +The new expression system provides **full control over facial expressions** by generating separate sprite assets for each expression type. This allows for smooth, dynamic expression switching based on face tracking data. + +## Key Changes + +### 1. **Blank Base Character** +The AI now generates a character with a **blank face** (no eyes, no mouth, no eyebrows). This allows us to overlay expression assets without visual conflicts. + +### 2. **Expression Grid Layout** + +The generated sprite sheet uses a **3-row grid format**: + +``` +┌─────────────────────────────────────────────────────────┐ +│ ROW 1: BASE CHARACTER (full width) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Blank face - no features (hair, head, body only) │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ ROW 2: EYE EXPRESSIONS (6 variants, equal spacing) │ +│ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ +│ │NTRL │HPPY │SRPR │ANGRY│ SAD │BLINK│ │ +│ └─────┴─────┴─────┴─────┴─────┴─────┘ │ +├─────────────────────────────────────────────────────────┤ +│ ROW 3: MOUTH EXPRESSIONS (6 variants, equal spacing) │ +│ ┌─────┬─────┬──────┬──────┬──────┬──────┐ │ +│ │NTRL │SMILE│TALK │WIDE │FROWN │O-SHP │ │ +│ └─────┴─────┴──────┴──────┴──────┴──────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3. **Expression Types** + +#### Eye Expressions (6 types) +| Type | Description | Trigger | +|------|-------------|---------| +| `NEUTRAL` | Normal open eyes, relaxed | Default state | +| `HAPPY` | Eyes curved upward, slightly closed | Smile detection (future) | +| `SURPRISED` | Wide open, circular eyes | Mouth open > 70% | +| `ANGRY` | Eyebrows angled down, narrowed | Emotion detection (future) | +| `SAD` | Eyebrows up, eyes droopy downward | Emotion detection (future) | +| `BLINK` | Both eyes fully closed (curves) | Blink detection (active) | + +#### Mouth Expressions (6 types) +| Type | Description | Trigger | +|------|-------------|---------| +| `NEUTRAL` | Small closed mouth, straight line | Mouth open < 10% | +| `HAPPY` | Closed mouth curved upward | Smile detection (future) | +| `OPEN_TALK` | Medium open mouth for vowels | Mouth open 10-30% | +| `WIDE_OPEN` | Large open mouth for shouting | Mouth open > 30% | +| `FROWN` | Mouth curved downward | Emotion detection (future) | +| `O_SHAPE` | Small circular open mouth | Phoneme detection (future) | + +## File Changes + +### `src/shared/types.ts` +```typescript +export enum ExpressionType { + NEUTRAL = 'NEUTRAL', + HAPPY = 'HAPPY', + SURPRISED = 'SURPRISED', + ANGRY = 'ANGRY', + SAD = 'SAD', + BLINK = 'BLINK', + OPEN_TALK = 'OPEN_TALK', + WIDE_OPEN = 'WIDE_OPEN', + FROWN = 'FROWN', + O_SHAPE = 'O_SHAPE', +} + +export interface AvatarConfig { + imageUrl: string; + baseFace?: Rect; // Blank face area + eyes?: { // Eye expression rects + [ExpressionType.NEUTRAL]?: Rect; + [ExpressionType.HAPPY]?: Rect; + // ... etc + }; + mouth?: { // Mouth expression rects + [ExpressionType.NEUTRAL]?: Rect; + [ExpressionType.HAPPY]?: Rect; + // ... etc + }; + riggingReference?: { ... }; + activeEyeExpression?: ExpressionType; + activeMouthExpression?: ExpressionType; +} +``` + +### `src/renderer/services/geminiService.ts` +Updated prompt to generate: +- Blank base character (no facial features) +- 6 eye expressions in row 2 +- 6 mouth expressions in row 3 +- Consistent sizing for easy extraction + +### `src/renderer/components/RiggingEditor.tsx` +Complete redesign: +- **Tab system**: Switch between Eyes and Mouth rigging +- **Expression selector**: Preview individual expressions +- **Color-coded boxes**: Each expression has unique color +- **Base Face box**: Define the blank character area +- **12 expression boxes total**: 6 eyes + 6 mouths + +### `src/renderer/components/Studio.tsx` +Dynamic expression rendering: +- `getCurrentEyeExpression()`: Maps tracking data to eye expression +- `getCurrentMouthExpression()`: Maps mouth openness to mouth expression +- Automatic expression switching based on: + - Blink detection → `BLINK` + - Mouth openness → `NEUTRAL` / `OPEN_TALK` / `WIDE_OPEN` + - Surprise detection → `SURPRISED` (when mouth very open) + +## Expression Switching Logic + +### Current Implementation + +```typescript +// Eye expression selection +const getCurrentEyeExpression = (): ExpressionType => { + if (trackingData.isBlinkingLeft || trackingData.isBlinkingRight) { + return ExpressionType.BLINK; + } + + if (trackingData.mouthOpen > 0.7) { + return ExpressionType.SURPRISED; + } + + return ExpressionType.NEUTRAL; // Default +}; + +// Mouth expression selection +const getCurrentMouthExpression = (): ExpressionType => { + const mouthOpen = trackingData.mouthOpen; + + if (mouthOpen < 0.1) return ExpressionType.NEUTRAL; + if (mouthOpen < 0.3) return ExpressionType.OPEN_TALK; + return ExpressionType.WIDE_OPEN; +}; +``` + +### Expression Flow + +``` +┌──────────────────┐ +│ Face Tracking │ +│ Data Input │ +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ mouthOpen: 0.05 │──────┐ +│ isBlinking: true │ │ +└────────┬─────────┘ │ + │ │ + ▼ ▼ +┌──────────────────┐ ┌──────────────┐ +│ Eye Expression │ │ Mouth │ +│ Selector │ │ Expression │ +│ │ │ Selector │ +│ BLINK (priority) │ │ NEUTRAL │ +└────────┬─────────┘ └──────┬───────┘ + │ │ + └────────┬──────────┘ + │ + ▼ + ┌────────────────┐ + │ Render Avatar │ + │ with selected │ + │ expressions │ + └────────────────┘ +``` + +## Rigging Workflow + +### Step 1: Generate Avatar +``` +User enters prompt → AI generates sprite sheet with: +- Row 1: Blank character +- Row 2: 6 eye expressions +- Row 3: 6 mouth expressions +``` + +### Step 2: Rig Expressions +``` +1. Adjust Base Face box (yellow) around blank character +2. Switch to "Eyes" tab +3. For each eye expression: + - Click expression name to highlight + - Drag/resizing box to match asset +4. Switch to "Mouth" tab +5. For each mouth expression: + - Click expression name to highlight + - Drag/resize box to match asset +6. Click "Finish Rigging" +``` + +### Step 3: Live Animation +``` +System automatically switches expressions based on: +- Your blinks → Eye BLINK +- Your mouth opening → Mouth OPEN_TALK / WIDE_OPEN +- Wide mouth → Eye SURPRISED +``` + +## Future Enhancements + +### Planned Features + +1. **Manual Expression Override** + - Hotkeys to force specific expressions + - Emotion wheel UI for manual selection + +2. **Advanced Triggers** + ```typescript + // Future: Audio-based phoneme detection + if (phoneme === 'AH') return ExpressionType.OPEN_TALK; + if (phoneme === 'OO') return ExpressionType.O_SHAPE; + + // Future: Eyebrow tracking + if (eyebrowsRaised) return ExpressionType.SURPRISED; + if (eyebrowsFurrowed) return ExpressionType.ANGRY; + ``` + +3. **Expression Blending** + - Smooth transitions between expressions + - Intensity-based blending (e.g., 50% happy + 50% neutral) + +4. **Preset Management** + - Save expression configurations + - Share rigging presets between avatars + +5. **More Expressions** + - Additional eye variants (wink, heart eyes, etc.) + - Mouth shapes for specific phonemes + - Eyebrow-only expressions layer + +## Testing Tips + +### During Rigging +1. **Zoom in** on sprite sheet for precise box placement +2. **Use consistent sizes** for similar expression types +3. **Test all expressions** by clicking through them before finishing +4. **Check the cyan face reference guide** - it should encompass the face area + +### During Studio Use +1. **Wait for calibration** (1 second after camera starts) +2. **Good lighting** improves expression detection +3. **Center your face** in camera for best results +4. **Exaggerate expressions** initially to test range + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Expressions don't align | Re-rig with more precise box placement | +| Blinking not detected | Increase camera lighting, face camera directly | +| Mouth stuck open | Check mouthOpen threshold in Studio.tsx | +| Wrong expression showing | Verify riggingReference calculation in RiggingEditor | +| Expressions too small/large | Ensure all expression assets are same size in sprite sheet | + +## Code Architecture + +``` +src/ +├── shared/types.ts # ExpressionType enum, AvatarConfig interface +├── renderer/ +│ ├── services/ +│ │ └── geminiService.ts # AI prompt for expression generation +│ ├── components/ +│ │ ├── AvatarCreator.tsx # Generate/upload avatar +│ │ ├── RiggingEditor.tsx # Rig all expressions +│ │ └── Studio.tsx # Dynamic expression switching +│ └── hooks/ +│ └── useFaceTracking.ts # Provides trackingData for triggers +``` + +## Summary + +The new expression system provides: +- ✅ **Full control** over all facial features +- ✅ **Dynamic switching** based on face tracking +- ✅ **Modular design** - easy to add new expressions +- ✅ **Clean separation** - blank base + overlay expressions +- ✅ **Future-proof** - ready for audio/emotion integration + +This is a **major improvement** over the previous 2-expression system (just blink/talk) and enables professional-quality VTuber animations. diff --git a/RIGGING_IMPROVEMENTS.md b/RIGGING_IMPROVEMENTS.md new file mode 100644 index 0000000..227bce2 --- /dev/null +++ b/RIGGING_IMPROVEMENTS.md @@ -0,0 +1,167 @@ +# Rigging System Improvements + +## Problem +The original rigging system had a **huge mess** in coordinate mapping between: +- Avatar image coordinates (from rigging editor) +- MediaPipe face tracking coordinates (from webcam) + +This caused avatar features to not align properly with the user's face movements. + +## Solution Overview + +### 1. **Face Reference System** (`src/shared/types.ts`) +Added `riggingReference` to `AvatarConfig`: +```typescript +riggingReference?: { + faceCenter: { x: number; y: number }; // Center point between eyes + faceWidth: number; // Normalized width of face at eye level + faceHeight: number; // Normalized height from brow to chin +}; +``` + +### 2. **Rigging Editor Calculations** (`src/renderer/components/RiggingEditor.tsx`) + +The editor now calculates face reference points when rigging is complete: + +```typescript +const calculateRiggingReference = () => { + // Face center is midpoint between eyes + const faceCenterX = (leftEye.x + leftEye.w / 2 + rightEye.x + rightEye.w / 2) / 2; + const faceCenterY = (leftEye.y + leftEye.h / 2 + rightEye.y + rightEye.h / 2) / 2; + + // Face width is distance between eye centers (normalized) + const faceWidth = Math.abs(rightEyeCenter - leftEyeCenter) * 2.5; + + // Face height from brow to chin + const faceHeight = chinY - browY; + + return { faceCenter, faceWidth, faceHeight }; +}; +``` + +**Visual Guide**: A cyan dashed box shows the calculated "Face Reference Area" during rigging. + +### 3. **Auto-Calibration** (`src/renderer/components/Studio.tsx`) + +On first face detection, the system: +1. Waits 1 second for stable tracking +2. Stores initial face position as `calibrationOffset` +3. All subsequent movements are **relative** to this offset + +```typescript +const relX = trackingData.translationX - calibrationOffset.x; +const relY = trackingData.translationY - calibrationOffset.y; +``` + +### 4. **Feature Position Mapping** (`src/renderer/components/Studio.tsx`) + +Features are now positioned relative to the face center: + +```typescript +const calculateFeaturePosition = (featureRect: Rect, featureType: 'eye' | 'mouth') => { + const { faceCenter, faceWidth, faceHeight } = avatar.riggingReference; + + // Calculate feature position relative to face center in rigging space + const relX = featureCenterX - faceCenter.x; + const relY = featureCenterY - faceCenter.y; + + // Scale relative positions by face width/height to match tracking scale + const scaledX = relX * faceWidth * avatarPosition.scale; + const scaledY = relY * faceHeight * avatarPosition.scale; + + return { x: scaledX, y: scaledY }; +}; +``` + +### 5. **Exponential Smoothing** (`src/renderer/hooks/useFaceTracking.ts`) + +Added smooth interpolation to prevent jittery movements: + +```typescript +const smoothingFactor = 0.15; // Lower = smoother but more lag + +const smooth = (current: number, target: number) => { + return current + (target - current) * smoothingFactor; +}; + +// Apply to all continuous values +const smoothedData = { + rotationX: smooth(prevDataRef.current.rotationX, newData.rotationX), + rotationY: smooth(prevDataRef.current.rotationY, newData.rotationY), + // ... etc +}; +``` + +Also improved blink detection threshold from `0.5` to `0.6` for more reliable blinks. + +## Coordinate Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ RIGGING PHASE │ +│ ┌─────────────────┐ │ +│ │ Avatar Image │ User places boxes on: │ +│ │ (Normalized) │ - Left/Right Eye (Red/Blue) │ +│ │ 0-1 coords │ - Mouth (Green) │ +│ └────────┬────────┘ - Main Body (Yellow) │ +│ │ │ +│ ▼ │ +│ Calculate riggingReference: │ +│ - faceCenter (between eyes) │ +│ - faceWidth (eye distance × 2.5) │ +│ - faceHeight (brow to chin) │ +└───────────┬─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ STUDIO PHASE │ +│ ┌─────────────────┐ │ +│ │ Webcam Feed │ MediaPipe detects: │ +│ │ (Real-time) │ - translationX/Y (-1 to 1) │ +│ │ │ - rotationX/Y/Z │ +│ └────────┬────────┘ - mouthOpen, blink │ +│ │ │ +│ ▼ │ +│ 1. Auto-calibrate (store initial offset) │ +│ 2. Calculate relative movement │ +│ 3. Apply smoothing (EMA with α=0.15) │ +│ 4. Map rigging coords to tracking scale │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ Render Avatar │ - Position from tracking │ +│ │ (Composited) │ - Features from riggingReference │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Key Benefits + +| Before | After | +|--------|-------| +| ❌ Fixed positions | ✅ Dynamic face-relative positioning | +| ❌ No calibration | ✅ Auto-calibration on startup | +| ❌ Jittery movement | ✅ Smooth exponential interpolation | +| ❌ No visual feedback | ✅ Face reference guide during rigging | +| ❌ Unreliable blinks | ✅ Improved blink threshold (0.6) | +| ❌ Scale mismatches | ✅ Proper scale mapping via faceWidth/Height | + +## Testing Tips + +1. **Rigging Phase**: + - Ensure the cyan "Face Reference Area" encompasses the entire face + - Eye boxes should be centered on pupils + - Mouth box should cover the lip area + +2. **Studio Phase**: + - Wait for "Calibrating..." indicator to disappear + - Start with face centered in camera + - Move head slowly to test tracking range + +## Future Improvements + +- [ ] Manual calibration button for re-centering +- [ ] Adjustable smoothing factor (UI slider) +- [ ] Face outline overlay for alignment verification +- [ ] Multiple face support +- [ ] Save/load rigging presets diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7a36465 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1869 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@google/genai': + specifier: 2.7.0 + version: 2.7.0 + '@mediapipe/tasks-vision': + specifier: 0.10.35 + version: 0.10.35 + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + devDependencies: + '@tailwindcss/postcss': + specifier: 4.3.0 + version: 4.3.0 + '@tailwindcss/vite': + specifier: 4.3.0 + version: 4.3.0(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0)) + '@types/node': + specifier: 25.9.1 + version: 25.9.1 + '@vitejs/plugin-react': + specifier: 6.0.2 + version: 6.0.2(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0)) + autoprefixer: + specifier: 10.5.0 + version: 10.5.0(postcss@8.5.15) + concurrently: + specifier: 10.0.3 + version: 10.0.3 + dotenv: + specifier: 17.4.2 + version: 17.4.2 + electron: + specifier: 42.3.3 + version: 42.3.3 + postcss: + specifier: 8.5.15 + version: 8.5.15 + tailwindcss: + specifier: 4.3.0 + version: 4.3.0 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: 8.0.16 + version: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + wait-on: + specifier: 9.0.10 + version: 9.0.10 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@electron/get@5.0.0': + resolution: {integrity: sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==} + engines: {node: '>=22.12.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@google/genai@2.7.0': + resolution: {integrity: sha512-tv0DRtcndt2oEhBYy+5mA0TaXH98+L1Gt0AP9unBfH7DP20KhB7+O3QqAN1Lz+laMARGTHS7BFQSNpLbl4gm1g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@hapi/address@5.1.1': + resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} + engines: {node: '>=14.0.0'} + + '@hapi/formula@3.0.2': + resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/tlds@1.1.6': + resolution: {integrity: sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mediapipe/tasks-vision@0.10.35': + resolution: {integrity: sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.0': + resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-generator-function@1.0.0: + resolution: {integrity: sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.17.0: + resolution: {integrity: sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concurrently@10.0.3: + resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} + engines: {node: '>=22'} + hasBin: true + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.366: + resolution: {integrity: sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==} + + electron@42.3.3: + resolution: {integrity: sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ==} + engines: {node: '>= 22.12.0'} + hasBin: true + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.1: + resolution: {integrity: sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + joi@18.2.1: + resolution: {integrity: sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==} + engines: {node: '>= 20'} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + protobufjs@7.6.2: + resolution: {integrity: sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==} + engines: {node: '>=12.0.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@7.27.0: + resolution: {integrity: sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==} + engines: {node: '>=20.18.1'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + wait-on@9.0.10: + resolution: {integrity: sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==} + engines: {node: '>=20.0.0'} + hasBin: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@electron/get@5.0.0': + dependencies: + debug: 4.4.3 + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.1 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.27.0 + transitivePeerDependencies: + - supports-color + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@google/genai@2.7.0': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.6.2 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@hapi/address@5.1.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/formula@3.0.2': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/pinpoint@2.0.1': {} + + '@hapi/tlds@1.1.6': {} + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mediapipe/tasks-vision@0.10.35': {} + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/postcss@4.3.0': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + postcss: 8.5.15 + tailwindcss: 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/retry@0.12.0': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.9.1 + optional: true + + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + async-function@1.0.0: {} + + async-generator-function@1.0.0: {} + + asynckit@0.4.0: {} + + autoprefixer@10.5.0(postcss@8.5.15): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + + axios@1.17.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.33: {} + + bignumber.js@9.3.1: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.366 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + caniuse-lite@1.0.30001793: {} + + chalk@5.6.2: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concurrently@10.0.3: + dependencies: + chalk: 5.6.2 + rxjs: 7.8.2 + shell-quote: 1.8.4 + supports-color: 10.2.2 + tree-kill: 1.2.2 + yargs: 18.0.0 + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.366: {} + + electron@42.3.3: + dependencies: + '@electron/get': 5.0.0 + '@types/node': 24.10.1 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + emoji-regex@10.6.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@3.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.1 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: {} + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + generator-function@2.0.1: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.1: + dependencies: + async-function: 1.0.0 + async-generator-function: 1.0.0 + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + generator-function: 2.0.1 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + jiti@2.7.0: {} + + joi@18.2.1: + dependencies: + '@hapi/address': 5.1.1 + '@hapi/formula': 3.0.2 + '@hapi/hoek': 11.0.7 + '@hapi/pinpoint': 2.0.1 + '@hapi/tlds': 1.1.6 + '@hapi/topo': 6.0.2 + '@standard-schema/spec': 1.1.0 + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash@4.18.1: {} + + long@5.3.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.47: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + progress@2.0.3: {} + + protobufjs@7.6.2: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 25.9.1 + long: 5.3.2 + + proxy-from-env@2.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + retry@0.13.1: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + scheduler@0.27.0: {} + + semver@7.8.1: {} + + shell-quote@1.8.4: {} + + source-map-js@1.2.1: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + supports-color@10.2.2: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + undici-types@7.24.6: {} + + undici@7.27.0: + optional: true + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + wait-on@9.0.10: + dependencies: + axios: 1.17.0 + joi: 18.2.1 + lodash: 4.18.1 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + - supports-color + + web-streams-polyfill@3.3.3: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + y18n@5.0.8: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 251a893..4efd0d3 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -77,23 +77,25 @@ const App: React.FC = () => { }; const handleRiggingComplete = (data: { - leftEye: Rect, rightEye: Rect, mouth: Rect, skinColor: string, - textureClosedEye: Rect, textureOpenMouth: Rect, - mainBody: Rect, chromaKeyColor: string + baseFace: Rect; + eyes: { [key: string]: Rect }; + mouth: { [key: string]: Rect }; + skinColor: string; + riggingReference: { faceCenter: { x: number; y: number }; faceWidth: number; faceHeight: number } }) => { if (generatedData) { setAvatar({ imageUrl: generatedData.url, name: generatedData.name, description: '', - leftEye: data.leftEye, - rightEye: data.rightEye, + baseFace: data.baseFace, + eyes: data.eyes, mouth: data.mouth, skinColor: data.skinColor, - textureClosedEye: data.textureClosedEye, - textureOpenMouth: data.textureOpenMouth, - mainBody: data.mainBody, - chromaKeyColor: data.chromaKeyColor + chromaKeyColor: 'AI_AUTO', + riggingReference: data.riggingReference, + activeEyeExpression: undefined, + activeMouthExpression: undefined }); setAppState(AppState.STUDIO); } diff --git a/src/renderer/components/AvatarCreator.tsx b/src/renderer/components/AvatarCreator.tsx index e8cab57..4bacb97 100644 --- a/src/renderer/components/AvatarCreator.tsx +++ b/src/renderer/components/AvatarCreator.tsx @@ -1,9 +1,7 @@ import React, { useState } from 'react'; -import { analyzeAvatarImage } from '../services/visionService'; import { stitchAssets, fileToDataUrl } from '../services/imageService'; import { generateAvatarImage } from '../services/geminiService'; import LoadingSpinner from './LoadingSpinner'; -import { Rect } from '../../shared/types'; const placeholderGenerate = async (prompt: string) => { const text = encodeURIComponent((prompt || '').substring(0, 80)); @@ -12,8 +10,7 @@ const placeholderGenerate = async (prompt: string) => { interface AvatarCreatorProps { onAvatarGenerated: (url: string, name: string, initialData?: { - leftEye?: Rect, rightEye?: Rect, mouth?: Rect, skinColor?: string, - mainBody?: Rect, textureClosedEye?: Rect, textureOpenMouth?: Rect + skinColor?: string }) => void; } @@ -22,7 +19,7 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => { const [prompt, setPrompt] = useState(''); const [name, setName] = useState(''); - const [status, setStatus] = useState<'idle' | 'generating' | 'analyzing' | 'stitching'>('idle'); + const [status, setStatus] = useState<'idle' | 'generating' | 'stitching'>('idle'); const [error, setError] = useState(null); const [baseFile, setBaseFile] = useState(null); @@ -38,14 +35,8 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => { try { const imageUrl = await generateAvatarImage(prompt); - setStatus('analyzing'); - const analysisData = await analyzeAvatarImage(imageUrl); - - if (analysisData) { - onAvatarGenerated(imageUrl, name, analysisData); - } else { - onAvatarGenerated(imageUrl, name); - } + // No automatic analysis - user will rig expressions manually + onAvatarGenerated(imageUrl, name, {}); } catch (err) { console.error(err); setError("Failed to generate avatar. Please try again."); @@ -62,58 +53,18 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => { try { const baseDataUrl = await fileToDataUrl(baseFile); - const baseAnalysis = await analyzeAvatarImage(baseDataUrl); - - let blinkDataUrl, blinkAnalysis; - if (blinkFile) { - blinkDataUrl = await fileToDataUrl(blinkFile); - blinkAnalysis = await analyzeAvatarImage(blinkDataUrl); + + // Simple stitch if blink/talk files provided, otherwise just use base + let imageUrl = baseDataUrl; + + if (blinkFile || talkFile) { + const blinkDataUrl = blinkFile ? await fileToDataUrl(blinkFile) : undefined; + const talkDataUrl = talkFile ? await fileToDataUrl(talkFile) : undefined; + const result = await stitchAssets(baseDataUrl, blinkDataUrl, talkDataUrl); + imageUrl = result.imageUrl; } - let talkDataUrl, talkAnalysis; - if (talkFile) { - talkDataUrl = await fileToDataUrl(talkFile); - talkAnalysis = await analyzeAvatarImage(talkDataUrl); - } - - const { imageUrl, mainBody, textureClosedEye: stitchBlinkRect, textureOpenMouth: stitchTalkRect } = await stitchAssets(baseDataUrl, blinkDataUrl, talkDataUrl); - - const mapRect = (r: Rect, container: Rect) => ({ - x: container.x + r.x * container.w, - y: container.y + r.y * container.h, - w: r.w * container.w, - h: r.h * container.h - }); - - let initialData: any = { - mainBody, - textureClosedEye: stitchBlinkRect, - textureOpenMouth: stitchTalkRect - }; - - if (baseAnalysis) { - initialData.leftEye = mapRect(baseAnalysis.leftEye, mainBody); - initialData.rightEye = mapRect(baseAnalysis.rightEye, mainBody); - initialData.mouth = mapRect(baseAnalysis.mouth, mainBody); - initialData.skinColor = baseAnalysis.skinColor; - } - - if (blinkAnalysis && stitchBlinkRect) { - const be = blinkAnalysis; - const minX = Math.min(be.leftEye.x, be.rightEye.x); - const minY = Math.min(be.leftEye.y, be.rightEye.y); - const maxX = Math.max(be.leftEye.x + be.leftEye.w, be.rightEye.x + be.rightEye.w); - const maxY = Math.max(be.leftEye.y + be.leftEye.h, be.rightEye.y + be.rightEye.h); - - const eyesRect = { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; - initialData.textureClosedEye = mapRect(eyesRect, stitchBlinkRect); - } - - if (talkAnalysis && stitchTalkRect) { - initialData.textureOpenMouth = mapRect(talkAnalysis.mouth, stitchTalkRect); - } - - onAvatarGenerated(imageUrl, name, initialData); + onAvatarGenerated(imageUrl, name, {}); } catch (err) { console.error(err); setError("Failed to process uploaded images. Please ensure they are valid image files."); @@ -160,8 +111,8 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => {

{mode === 'generate' - ? 'Describe your dream VTuber model. Gemini will generate a character sheet with expression assets.' - : 'Upload your existing character art. We support separate files for blink and talk variants.' + ? 'Describe your dream VTuber model. Gemini will generate a character sheet with multiple expression assets.' + : 'Upload your existing character art. Supports separate files for different expressions.' }

@@ -187,23 +138,28 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => { placeholder="e.g., A cyberpunk anime girl with neon blue hair, glowing headphones, wearing a futuristic jacket..." className="w-full h-32 bg-slate-900/50 border border-slate-600 rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition-all outline-none resize-none" /> +

+ 💡 The AI will generate a sprite sheet with 6 eye expressions and 6 mouth expressions, plus a blank base face. +

) : (
handleFileChange(e, setBaseFile)} className="text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-cyan-500/10 file:text-cyan-400 hover:file:bg-cyan-500/20"/> -

The main look of your character (Eyes Open, Mouth Closed).

+

The main look of your character with blank face (no features).

- + handleFileChange(e, setBlinkFile)} className="text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-purple-500/10 file:text-purple-400 hover:file:bg-purple-500/20"/> +

Sprite sheet with all eye/mouth expressions.

- + handleFileChange(e, setTalkFile)} className="text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-pink-500/10 file:text-pink-400 hover:file:bg-pink-500/20"/> +

Additional expression variants.

@@ -228,9 +184,7 @@ const AvatarCreator: React.FC = ({ onAvatarGenerated }) => {
- {status === 'generating' ? 'Dreaming up Sheet...' : - status === 'stitching' ? 'Processing Assets...' : - 'Analyzing Features...'} + {status === 'generating' ? 'Dreaming up Character...' : 'Processing Assets...'}
) : ( diff --git a/src/renderer/components/RiggingEditor.tsx b/src/renderer/components/RiggingEditor.tsx index 9bf67ff..0c569b3 100644 --- a/src/renderer/components/RiggingEditor.tsx +++ b/src/renderer/components/RiggingEditor.tsx @@ -1,17 +1,41 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Rect } from '../../shared/types'; +import { Rect, ExpressionType } from '../../shared/types'; interface RiggingEditorProps { imageUrl: string; - initialData?: { leftEye: Rect; rightEye: Rect; mouth: Rect; skinColor: string }; + initialData?: { + baseFace?: Rect; + eyes?: { [key: string]: Rect }; + mouth?: { [key: string]: Rect }; + skinColor?: string + }; onComplete: (data: { - leftEye: Rect; rightEye: Rect; mouth: Rect; skinColor: string; - textureClosedEye: Rect; textureOpenMouth: Rect; - mainBody: Rect; chromaKeyColor: string; + baseFace: Rect; + eyes: { [key: string]: Rect }; + mouth: { [key: string]: Rect }; + skinColor: string; + riggingReference: { faceCenter: { x: number; y: number }; faceWidth: number; faceHeight: number }; }) => void; } -type ActiveFeature = 'leftEye' | 'rightEye' | 'mouth' | 'textureClosedEye' | 'textureOpenMouth' | 'mainBody' | null; +type ActiveFeature = + | 'baseFace' + | `eye-${ExpressionType}` + | `mouth-${ExpressionType}` + | null; + +const EXPRESSION_LABELS: Record = { + [ExpressionType.NEUTRAL]: 'Neutral', + [ExpressionType.HAPPY]: 'Happy', + [ExpressionType.SURPRISED]: 'Surprised', + [ExpressionType.ANGRY]: 'Angry', + [ExpressionType.SAD]: 'Sad', + [ExpressionType.BLINK]: 'Blink', + [ExpressionType.OPEN_TALK]: 'Talk', + [ExpressionType.WIDE_OPEN]: 'Wide', + [ExpressionType.FROWN]: 'Frown', + [ExpressionType.O_SHAPE]: 'O-Shape', +}; const ResizableBox: React.FC<{ rect: Rect; @@ -20,7 +44,8 @@ const ResizableBox: React.FC<{ isActive: boolean; onUpdate: (rect: Rect) => void; onActivate: () => void; -}> = ({ rect, color, label, isActive, onUpdate, onActivate }) => { + visible?: boolean; +}> = ({ rect, color, label, isActive, onUpdate, onActivate, visible = true }) => { const boxRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -87,6 +112,8 @@ const ResizableBox: React.FC<{ }; }, [isDragging, isResizing, rect, onUpdate]); + if (!visible) return null; + return (
= ({ imageUrl, initialData, onComplete }) => { - const [leftEye, setLeftEye] = useState(initialData?.leftEye || { x: 0.25, y: 0.4, w: 0.1, h: 0.1 }); - const [rightEye, setRightEye] = useState(initialData?.rightEye || { x: 0.45, y: 0.4, w: 0.1, h: 0.1 }); - const [mouth, setMouth] = useState(initialData?.mouth || { x: 0.35, y: 0.55, w: 0.1, h: 0.05 }); - - const [mainBody, setMainBody] = useState({ x: 0.05, y: 0.05, w: 0.65, h: 0.9 }); + // Base face (blank character) + const [baseFace, setBaseFace] = useState(initialData?.baseFace || { x: 0.05, y: 0.05, w: 0.65, h: 0.9 }); - const [textureClosedEye, setTextureClosedEye] = useState({ x: 0.7, y: 0.1, w: 0.2, h: 0.2 }); - const [textureOpenMouth, setTextureOpenMouth] = useState({ x: 0.7, y: 0.5, w: 0.2, h: 0.2 }); + // Eye expressions - initialize with defaults + const defaultEyeRect: Rect = { x: 0.7, y: 0.05, w: 0.25, h: 0.15 }; + const [eyes, setEyes] = useState<{ [key: string]: Rect }>({ + [ExpressionType.NEUTRAL]: initialData?.eyes?.[ExpressionType.NEUTRAL] || { ...defaultEyeRect, y: 0.05 }, + [ExpressionType.HAPPY]: initialData?.eyes?.[ExpressionType.HAPPY] || { ...defaultEyeRect, y: 0.22 }, + [ExpressionType.SURPRISED]: initialData?.eyes?.[ExpressionType.SURPRISED] || { ...defaultEyeRect, y: 0.39 }, + [ExpressionType.ANGRY]: initialData?.eyes?.[ExpressionType.ANGRY] || { ...defaultEyeRect, y: 0.56 }, + [ExpressionType.SAD]: initialData?.eyes?.[ExpressionType.SAD] || { ...defaultEyeRect, y: 0.73 }, + [ExpressionType.BLINK]: initialData?.eyes?.[ExpressionType.BLINK] || { ...defaultEyeRect, y: 0.90 }, + }); + + // Mouth expressions - initialize with defaults + const defaultMouthRect: Rect = { x: 0.7, y: 0.05, w: 0.25, h: 0.15 }; + const [mouth, setMouth] = useState<{ [key: string]: Rect }>({ + [ExpressionType.NEUTRAL]: initialData?.mouth?.[ExpressionType.NEUTRAL] || { ...defaultMouthRect, y: 0.05 }, + [ExpressionType.HAPPY]: initialData?.mouth?.[ExpressionType.HAPPY] || { ...defaultMouthRect, y: 0.22 }, + [ExpressionType.OPEN_TALK]: initialData?.mouth?.[ExpressionType.OPEN_TALK] || { ...defaultMouthRect, y: 0.39 }, + [ExpressionType.WIDE_OPEN]: initialData?.mouth?.[ExpressionType.WIDE_OPEN] || { ...defaultMouthRect, y: 0.56 }, + [ExpressionType.FROWN]: initialData?.mouth?.[ExpressionType.FROWN] || { ...defaultMouthRect, y: 0.73 }, + [ExpressionType.O_SHAPE]: initialData?.mouth?.[ExpressionType.O_SHAPE] || { ...defaultMouthRect, y: 0.90 }, + }); const [skinColor, setSkinColor] = useState(initialData?.skinColor || '#fcd3bf'); - const [useAiBackground, setUseAiBackground] = useState(true); - const [activeFeature, setActiveFeature] = useState(null); + const [activeTab, setActiveTab] = useState<'eyes' | 'mouth'>('eyes'); + const [visibleExpression, setVisibleExpression] = useState(ExpressionType.NEUTRAL); + + // Calculate face reference points for mapping rigging to tracking + const calculateRiggingReference = () => { + // Use the neutral eye expression as reference + const neutralEyes = eyes[ExpressionType.NEUTRAL]; + if (!neutralEyes) { + return { + faceCenter: { x: 0.5, y: 0.5 }, + faceWidth: 0.3, + faceHeight: 0.4 + }; + } + + // Assume eyes rect contains both eyes side by side + const faceCenterX = neutralEyes.x + neutralEyes.w / 2; + const faceCenterY = neutralEyes.y + neutralEyes.h / 2; + + // Face width is approximately 2.5x the eye width + const faceWidth = neutralEyes.w * 2.5; + + // Face height from brow to chin + const faceHeight = neutralEyes.h * 3.5; + + return { + faceCenter: { x: faceCenterX, y: faceCenterY }, + faceWidth, + faceHeight + }; + }; + + const updateEye = (type: ExpressionType, rect: Rect) => { + setEyes(prev => ({ ...prev, [type]: rect })); + }; + + const updateMouth = (type: ExpressionType, rect: Rect) => { + setMouth(prev => ({ ...prev, [type]: rect })); + }; + + const getExpressionColor = (type: ExpressionType, index: number) => { + const colors = ['#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']; + return colors[index % colors.length]; + }; return (

Rig Your Character

- 1. Adjust the Main Body (Yellow) to frame your character.
- 2. Match the Targets (Red/Blue/Green) to the face features.
- 3. Match the Sources (Purple/Orange) to the assets on the right. + 1. Adjust the Base Face (Yellow) to frame your blank character.
+ 2. For each expression, match the colored box to the corresponding asset on the right.
+ 3. Use tabs to switch between eye and mouth expressions.

@@ -154,60 +239,95 @@ const RiggingEditor: React.FC = ({ imageUrl, initialData, on />
+ {/* Base Face */} setActiveFeature('mainBody')} + rect={baseFace} + color="#facc15" + label="Base Face (Blank)" + isActive={activeFeature === 'baseFace'} + onUpdate={setBaseFace} + onActivate={() => setActiveFeature('baseFace')} /> - setActiveFeature('leftEye')} - /> - setActiveFeature('rightEye')} - /> - setActiveFeature('mouth')} - /> + {/* Eye expressions - only show active one on main image for clarity */} + {Object.entries(eyes).map(([type, rect], index) => ( + updateEye(type as ExpressionType, r)} + onActivate={() => setActiveFeature(`eye-${type}`)} + visible={activeTab === 'eyes'} + /> + ))} - setActiveFeature('textureClosedEye')} - /> - setActiveFeature('textureOpenMouth')} - /> + {/* Mouth expressions */} + {Object.entries(mouth).map(([type, rect], index) => ( + updateMouth(type as ExpressionType, r)} + onActivate={() => setActiveFeature(`mouth-${type}`)} + visible={activeTab === 'mouth'} + /> + ))}
-
+
+ {/* Expression type tabs */} +
+ + +
+ + {/* Expression selector */} +
+
+ {activeTab === 'eyes' ? 'Eye' : 'Mouth'} Expressions +
+ {(activeTab === 'eyes' ? Object.keys(eyes) : Object.keys(mouth)).map((type) => ( + + ))} +
+ + {/* Color picker */}
-
- -
- AI Magic Removal - -
-
@@ -222,39 +342,29 @@ const RiggingEditor: React.FC = ({ imageUrl, initialData, on
-
-
Composition
-
setActiveFeature('mainBody')}> -
Main Body Crop -
- -
Targets (Main Face)
-
setActiveFeature('leftEye')}> -
Left Eye -
-
setActiveFeature('rightEye')}> -
Right Eye -
-
setActiveFeature('mouth')}> -
Mouth -
- -
Sources (Right Side)
-
setActiveFeature('textureClosedEye')}> -
Closed Eye Texture -
-
setActiveFeature('textureOpenMouth')}> -
Open Mouth Texture -
+ {/* Instructions */} +
+
TIPS:
+
    +
  • • Click expression name to preview
  • +
  • • Drag boxes to position
  • +
  • • Use corner handle to resize
  • +
  • • Match boxes to expression assets on right side of sprite sheet
  • +
@@ -162,7 +275,7 @@ const Studio: React.FC = ({ avatar, onBack }) => {
-
+
{!processedImageUrl ? (
@@ -173,10 +286,11 @@ const Studio: React.FC = ({ avatar, onBack }) => { className="relative w-full h-full flex items-center justify-center" style={getAvatarStyle()} > - {avatar.mainBody ? ( + {/* Base blank face */} + {avatar.baseFace ? ( ) : ( @@ -187,64 +301,59 @@ const Studio: React.FC = ({ avatar, onBack }) => { /> )} - {avatar.leftEye && avatar.textureClosedEye && ( - - )} - - {avatar.rightEye && avatar.textureClosedEye && ( - + > + +
)} - {avatar.mouth && avatar.textureOpenMouth && ( + {/* Current mouth expression */} + {currentMouthRect && (
-
0.1 ? 1 : 0, - filter: 'blur(4px)', - borderRadius: '50%' - }} - /> + {/* Skin-colored backing for open mouth */} + {trackingData.mouthOpen > 0.1 && ( +
+ )} 0.05 ? 1 : 0, + opacity: trackingData.mouthOpen > 0.05 ? 1 : 0.3, transform: `scaleY(${0.8 + trackingData.mouthOpen * 0.5})`, }} /> @@ -291,6 +400,14 @@ const Studio: React.FC = ({ avatar, onBack }) => {
+ +
+ EXPRESSION +
+ {currentEyeExpression} + {currentMouthExpression} +
+
); diff --git a/src/renderer/hooks/useFaceTracking.ts b/src/renderer/hooks/useFaceTracking.ts index a0d555e..1254c36 100644 --- a/src/renderer/hooks/useFaceTracking.ts +++ b/src/renderer/hooks/useFaceTracking.ts @@ -8,6 +8,15 @@ export const useFaceTracking = (videoElement: HTMLVideoElement | null) => { const faceLandmarkerRef = useRef(null); const requestRef = useRef(null); const lastVideoTimeRef = useRef(-1); + + // Smoothing refs for exponential moving average + const smoothingFactor = 0.15; // Lower = smoother but more lag + const prevDataRef = useRef({ + rotationX: 0, rotationY: 0, rotationZ: 0, + translationX: 0, translationY: 0, mouthOpen: 0, + isBlinkingLeft: false, isBlinkingRight: false + }); + const [trackingData, setTrackingData] = useState({ rotationX: 0, rotationY: 0, @@ -26,7 +35,7 @@ export const useFaceTracking = (videoElement: HTMLVideoElement | null) => { "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm" ); - faceLandmarkerRef.current = await FaceLandmarker.createFromOptions(filesetResolver, { + faceLandmarkerRef.current = await FaceLandmarker.createFromOptions(filesetResolver, { baseOptions: { modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`, delegate: "GPU" @@ -94,16 +103,36 @@ export const useFaceTracking = (videoElement: HTMLVideoElement | null) => { const transX = (nose.x - 0.5) * 2; const transY = (nose.y - 0.5) * 2; - setTrackingData({ + // Apply exponential smoothing to continuous values + const smooth = (current: number, target: number) => { + return current + (target - current) * smoothingFactor; + }; + + const newData: TrackingData = { rotationZ: roll, rotationY: yaw, rotationX: pitch, translationX: transX, translationY: transY, mouthOpen, - isBlinkingLeft: eyeBlinkLeft > 0.5, - isBlinkingRight: eyeBlinkRight > 0.5 - }); + isBlinkingLeft: eyeBlinkLeft > 0.6, // Higher threshold for more reliable blink detection + isBlinkingRight: eyeBlinkRight > 0.6 + }; + + // Smooth the data + const smoothedData = { + rotationX: smooth(prevDataRef.current.rotationX, newData.rotationX), + rotationY: smooth(prevDataRef.current.rotationY, newData.rotationY), + rotationZ: smooth(prevDataRef.current.rotationZ, newData.rotationZ), + translationX: smooth(prevDataRef.current.translationX, newData.translationX), + translationY: smooth(prevDataRef.current.translationY, newData.translationY), + mouthOpen: smooth(prevDataRef.current.mouthOpen, newData.mouthOpen), + isBlinkingLeft: newData.isBlinkingLeft, + isBlinkingRight: newData.isBlinkingRight + }; + + prevDataRef.current = newData; + setTrackingData(smoothedData); } } requestRef.current = requestAnimationFrame(predict); diff --git a/src/renderer/services/geminiService.ts b/src/renderer/services/geminiService.ts index 96a2c66..3cd1498 100644 --- a/src/renderer/services/geminiService.ts +++ b/src/renderer/services/geminiService.ts @@ -40,18 +40,38 @@ export const generateAvatarImage = async (description: string): Promise const prompt = ` Create a VTuber character sheet with a flat 2D anime style. - LAYOUT: - 1. MAIN CHARACTER (Left side, takes up 70% of width): - - Front-facing view, head and shoulders. - - Neutral expression, eyes open, mouth closed. + IMPORTANT: The base character face should be BLANK - NO eyes, NO mouth, NO eyebrows. Just the face outline, hair, and body. We will overlay separate expression assets. - 2. EXPRESSION ASSETS (Right side, vertical column): - - Top: The same character's face with EYES CLOSED (for blinking). - - Bottom: The same character's face with MOUTH OPEN (for talking). + LAYOUT (grid format on white or green background): - Character Description: ${description} + ROW 1 - BASE CHARACTER (full width): + - Front-facing view, head and shoulders, BLANK FACE (no facial features) - Style: Vibrant, clean lines, solid white or green background for easy keying. + ROW 2 - EYE EXPRESSIONS (6 variants, equal spacing): + 1. NEUTRAL: Normal open eyes, relaxed + 2. HAPPY: Eyes curved upward in smile shape, slightly closed + 3. SURPRISED: Wide open, circular eyes with visible iris + 4. ANGRY: Eyebrows angled down, eyes narrowed with sharp shape + 5. SAD: Eyebrows angled up, eyes looking downward, slightly droopy + 6. BLINK: Both eyes fully closed (curved lines) + + ROW 3 - MOUTH EXPRESSIONS (6 variants, equal spacing): + 1. NEUTRAL: Small closed mouth, straight line + 2. SMILE: Closed mouth curved upward + 3. OPEN TALK: Medium open mouth for vowel sounds + 4. WIDE OPEN: Large open mouth for shouting/laughing + 5. FROWN: Mouth curved downward + 6. O-SHAPE: Small circular open mouth + + CHARACTER DESCRIPTION: ${description} + + STYLE REQUIREMENTS: + - All expressions should be the SAME SIZE for easy swapping + - Clean lines, solid colors, no shading on expressions + - Expressions should be on transparent or solid color background + - Eyes should include eyebrows as part of the eye asset + - Consistent art style across all variants + - High contrast for easy extraction `; const response = await ai.models.generateContent({ diff --git a/src/shared/types.ts b/src/shared/types.ts index 6029116..0a9bc43 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,3 +1,16 @@ +export enum ExpressionType { + NEUTRAL = 'NEUTRAL', + HAPPY = 'HAPPY', + SURPRISED = 'SURPRISED', + ANGRY = 'ANGRY', + SAD = 'SAD', + BLINK = 'BLINK', + OPEN_TALK = 'OPEN_TALK', + WIDE_OPEN = 'WIDE_OPEN', + FROWN = 'FROWN', + O_SHAPE = 'O_SHAPE', +} + export enum AppState { SETUP = 'SETUP', CREATION = 'CREATION', @@ -16,14 +29,38 @@ export interface AvatarConfig { imageUrl: string; name: string; description: string; - leftEye?: Rect; - rightEye?: Rect; - mouth?: Rect; + // Base face (blank, no features) + baseFace?: Rect; + // Eye expressions - each expression type maps to a rect on the sprite sheet + eyes?: { + [ExpressionType.NEUTRAL]?: Rect; + [ExpressionType.HAPPY]?: Rect; + [ExpressionType.SURPRISED]?: Rect; + [ExpressionType.ANGRY]?: Rect; + [ExpressionType.SAD]?: Rect; + [ExpressionType.BLINK]?: Rect; + }; + // Mouth expressions + mouth?: { + [ExpressionType.NEUTRAL]?: Rect; + [ExpressionType.HAPPY]?: Rect; + [ExpressionType.OPEN_TALK]?: Rect; + [ExpressionType.WIDE_OPEN]?: Rect; + [ExpressionType.FROWN]?: Rect; + [ExpressionType.O_SHAPE]?: Rect; + }; skinColor?: string; - textureClosedEye?: Rect; - textureOpenMouth?: Rect; - mainBody?: Rect; chromaKeyColor?: string; + // Reference points for mapping rigging to face tracking (normalized 0-1) + // These define where features should be relative to the face center + riggingReference?: { + faceCenter: { x: number; y: number }; // Center point between eyes + faceWidth: number; // Normalized width of face at eye level + faceHeight: number; // Normalized height from brow to chin + }; + // Currently active expressions (for studio) + activeEyeExpression?: ExpressionType; + activeMouthExpression?: ExpressionType; } export interface TrackingData {