From 1d152d01a11033ea0b075f539e903d3ae57bbb4e Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 5 Apr 2026 02:01:37 +0900 Subject: [PATCH] =?UTF-8?q?Phase=201+2=20=EA=B5=AC=ED=98=84:=20Electron=20?= =?UTF-8?q?=EB=BC=88=EB=8C=80=20+=20STT/=ED=95=AB=ED=82=A4/=EC=98=A4?= =?UTF-8?q?=EC=BC=80=EC=8A=A4=ED=8A=B8=EB=A0=88=EC=9D=B4=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: - 프로젝트 초기화 (TypeScript strict, electron-vite, ESLint, Prettier) - shared 타입 (ipc-channels 113채널, types, errors, constants) - 메인 프로세스 뼈대 (bootstrap, lifecycle, 단일 인스턴스) - LoggerService, ConfigService (electron-store ESM dynamic import) - React 19 + MUI 7 Dashboard, 시스템 트레이 Phase 2: - AudioCaptureService (node-record-lpcm16, PCM16 16kHz mono) - HotkeyService (uiohook-napi, 더블프레스, holdMode/toggleMode) - LocalSTTService (faster-whisper Python sidecar, 이중 조건 플러시) - VoiceModeService 오케스트레이터 (이중 상태머신, Action Queue) - Python sidecar (FastAPI: health/load/transcribe/shutdown) - IPC 핸들러 (voice, stt, hotkey) + Preload API 확장 --- .claude/skills/implement-phase/SKILL.md | 2 + .eslintrc.cjs | 25 + .gitignore | 12 + .prettierrc | 7 + CLAUDE.md | 26 +- electron.vite.config.ts | 30 + package-lock.json | 5590 ++++++++++++++++++++++ package.json | 50 + sidecar/main.py | 332 ++ sidecar/requirements.txt | 5 + src/main/bootstrap.ts | 78 + src/main/index.ts | 34 + src/main/ipc/audio-handlers.ts | 37 + src/main/ipc/config-handlers.ts | 72 + src/main/ipc/hotkey-handlers.ts | 67 + src/main/ipc/index.ts | 23 + src/main/ipc/stt-handlers.ts | 52 + src/main/ipc/system-handlers.ts | 28 + src/main/ipc/voice-handlers.ts | 53 + src/main/ipc/window-handlers.ts | 31 + src/main/lifecycle.ts | 37 + src/main/services/AudioCaptureService.ts | 365 ++ src/main/services/ConfigService.ts | 119 + src/main/services/HotkeyService.ts | 536 +++ src/main/services/LocalSTTService.ts | 729 +++ src/main/services/LoggerService.ts | 32 + src/main/services/VoiceModeService.ts | 625 +++ src/main/services/index.ts | 26 + src/main/types/node-record-lpcm16.d.ts | 33 + src/main/windows/TrayManager.ts | 55 + src/main/windows/WindowManager.ts | 66 + src/preload/index.ts | 164 + src/renderer/App.tsx | 26 + src/renderer/components/AppLayout.tsx | 110 + src/renderer/electron.d.ts | 8 + src/renderer/index.html | 12 + src/renderer/main.tsx | 14 + src/renderer/pages/DashboardPage.tsx | 67 + src/renderer/theme.ts | 148 + src/shared/constants.ts | 58 + src/shared/errors.ts | 162 + src/shared/ipc-channels.ts | 172 + src/shared/types.ts | 658 +++ tsconfig.json | 7 + tsconfig.node.json | 25 + tsconfig.web.json | 24 + 46 files changed, 10828 insertions(+), 4 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 .prettierrc create mode 100644 electron.vite.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 sidecar/main.py create mode 100644 sidecar/requirements.txt create mode 100644 src/main/bootstrap.ts create mode 100644 src/main/index.ts create mode 100644 src/main/ipc/audio-handlers.ts create mode 100644 src/main/ipc/config-handlers.ts create mode 100644 src/main/ipc/hotkey-handlers.ts create mode 100644 src/main/ipc/index.ts create mode 100644 src/main/ipc/stt-handlers.ts create mode 100644 src/main/ipc/system-handlers.ts create mode 100644 src/main/ipc/voice-handlers.ts create mode 100644 src/main/ipc/window-handlers.ts create mode 100644 src/main/lifecycle.ts create mode 100644 src/main/services/AudioCaptureService.ts create mode 100644 src/main/services/ConfigService.ts create mode 100644 src/main/services/HotkeyService.ts create mode 100644 src/main/services/LocalSTTService.ts create mode 100644 src/main/services/LoggerService.ts create mode 100644 src/main/services/VoiceModeService.ts create mode 100644 src/main/services/index.ts create mode 100644 src/main/types/node-record-lpcm16.d.ts create mode 100644 src/main/windows/TrayManager.ts create mode 100644 src/main/windows/WindowManager.ts create mode 100644 src/preload/index.ts create mode 100644 src/renderer/App.tsx create mode 100644 src/renderer/components/AppLayout.tsx create mode 100644 src/renderer/electron.d.ts create mode 100644 src/renderer/index.html create mode 100644 src/renderer/main.tsx create mode 100644 src/renderer/pages/DashboardPage.tsx create mode 100644 src/renderer/theme.ts create mode 100644 src/shared/constants.ts create mode 100644 src/shared/errors.ts create mode 100644 src/shared/ipc-channels.ts create mode 100644 src/shared/types.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 tsconfig.web.json diff --git a/.claude/skills/implement-phase/SKILL.md b/.claude/skills/implement-phase/SKILL.md index 356abff..426eba7 100644 --- a/.claude/skills/implement-phase/SKILL.md +++ b/.claude/skills/implement-phase/SKILL.md @@ -28,6 +28,7 @@ allowed-tools: Read Write Edit Bash Glob Grep Agent - 프리로드 브릿지 업데이트 6. **테스트**: 각 모듈 단위 테스트 7. **CLAUDE.md 업데이트**: 현재 상태 갱신 +8. **Memory 갱신** (필수): `project_status.md`에 완료된 Phase, 작업 내역, 기술 결정 기록 ## 구현 규칙 (위반 불가) - 서비스는 **설계서 01**의 인터페이스를 그대로 구현 (메서드 시그니처, 이벤트 페이로드 일치) @@ -43,3 +44,4 @@ allowed-tools: Read Write Edit Bash Glob Grep Agent - `npm run typecheck` 통과 - `npm run test` 통과 - CLAUDE.md "현재 상태" 업데이트됨 +- Memory `project_status.md` 갱신됨 (Phase 완료 내역, 기술 결정, 다음 작업) diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..bc2269b --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,25 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + es2022: true + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + plugins: ['@typescript-eslint'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-console': 'error' + } +} diff --git a/.gitignore b/.gitignore index e9fc968..9acb248 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,15 @@ out/ .DS_Store Thumbs.db .claude/settings.local.json + +# Electron +release/ +*.unpacked + +# Build +*.tsbuildinfo + +# IDE +.idea/ +*.swp +*.swo diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..199debb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/CLAUDE.md b/CLAUDE.md index ad7142b..10efbd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,10 +77,28 @@ npm run typecheck # tsc --noEmit 6. **녹음 UI**: 9개 웨이브 바, cos 분포 가중치, 100ms 애니메이션 ## 현재 상태 -Phase: 0 (하네스 설정 완료, 구현 시작 전) -마지막 완료: 하네스 시스템 구축 -다음 작업: Phase 1 — 프로젝트 초기화 + Electron 뼈대 -차단 이슈: 없음 +Phase: 2 완료 +마지막 완료: Phase 2 — 로컬 STT (faster-whisper sidecar) + 핫키 (uiohook-napi) + VoiceModeService 오케스트레이터 +다음 작업: Phase 3 — 텍스트 삽입 + 기본 UI (RecordingTip, ResultPopup) +차단 이슈: SoX 미설치 시 AudioCaptureService 동작 불가 (수동 설치 필요) + +### Phase 2 구현 내용 +- AudioCaptureService: node-record-lpcm16 + SoX 실제 마이크 캡처 (PCM16 16kHz mono, 60ms 프레임, RMS 레벨) +- HotkeyService: uiohook-napi 글로벌 키후킹, 더블프레스(300ms), holdMode/toggleMode, VK→uiohook 매핑 +- LocalSTTService: faster-whisper Python sidecar 관리, STTState 상태머신, 이중 조건 플러시, 자동 재시작(3회) +- VoiceModeService: RecognitionState(9상태) + AudioState(4상태) 이중 상태머신, Action Queue 직렬화, accidentalPress(<700ms), Dictation/HandsFree 모드 +- Python sidecar: FastAPI (GET /health, POST /load, POST /transcribe, POST /shutdown) +- IPC 핸들러: voice, stt, hotkey 추가 (기존 audio, config, window, system에 추가) +- Preload: voice/stt/hotkey API 전체 노출 +- Bootstrap: 7단계 초기화 (logger→config→windows→tray→ipc→hotkey→voice-mode) + +### Phase 1 구현 내용 +- 프로젝트 초기화: package.json, TypeScript strict, electron-vite, ESLint, Prettier +- shared 타입: ipc-channels.ts (113채널), types.ts, errors.ts (ErrorCode enum), constants.ts +- 메인 프로세스: index.ts (단일 인스턴스), bootstrap.ts, lifecycle.ts +- 서비스: LoggerService, ConfigService (ESM dynamic import) +- Renderer: React 19 + MUI 7, AppLayout (Drawer 240px), DashboardPage +- 시스템 트레이, closeToTray ## 페이즈 로드맵 - Phase 1: 프로젝트 초기화 + Electron 뼈대 + 마이크 캡처 diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..d2703ed --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,30 @@ +import { resolve } from 'path' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + resolve: { + alias: { + '@shared': resolve('src/shared') + } + } + }, + preload: { + plugins: [externalizeDepsPlugin()], + resolve: { + alias: { + '@shared': resolve('src/shared') + } + } + }, + renderer: { + resolve: { + alias: { + '@shared': resolve('src/shared') + } + }, + plugins: [react()] + } +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1793583 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5590 @@ +{ + "name": "d3ro-voice", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "d3ro-voice", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.0", + "@mui/material": "^7.0.0", + "@rollup/rollup-win32-x64-msvc": "^4.60.1", + "electron-log": "^5.2.0", + "electron-store": "^10.0.0", + "node-record-lpcm16": "^1.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "uiohook-napi": "^1.5.5" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@types/node": "^22.13.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "electron": "^33.3.0", + "electron-vite": "^2.3.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.3.0", + "typescript": "^5.7.0", + "vite": "^5.4.0", + "vitest": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron-toolkit/preload": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-3.0.2.tgz", + "integrity": "sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==", + "license": "MIT", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron-toolkit/tsconfig": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", + "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/node": "*" + } + }, + "node_modules/@electron-toolkit/utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-4.0.0.tgz", + "integrity": "sha512-qXSntwEzluSzKl4z5yFNBknmPGjPa3zFhE4mp9+h0cgokY5ornAeP+CJQDBhKsL1S58aOQfcwkD3NwLZCl+64g==", + "license": "MIT", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", + "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.9.tgz", + "integrity": "sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.9", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", + "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/core-downloads-tracker": "^7.3.9", + "@mui/system": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.9", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz", + "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/utils": "^7.3.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz", + "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", + "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/private-theming": "^7.3.9", + "@mui/styled-engine": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz", + "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", + "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/conf": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz", + "integrity": "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^9.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron": { + "version": "33.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-log": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/electron-store": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-10.1.0.tgz", + "integrity": "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w==", + "license": "MIT", + "dependencies": { + "conf": "^14.0.0", + "type-fest": "^4.41.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", + "integrity": "sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "cac": "^6.7.14", + "esbuild": "^0.21.5", + "magic-string": "^0.30.10", + "picocolors": "^1.0.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-record-lpcm16": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-record-lpcm16/-/node-record-lpcm16-1.0.1.tgz", + "integrity": "sha512-H75GMOP8ErnF67m21+qSgj4USnzv5RLfm7OkEItdIi+soNKoJZpMQPX6umM8Cn9nVPSgd/dBUtc1msst5MmABA==", + "license": "ISC", + "dependencies": { + "debug": "^2.6.8" + } + }, + "node_modules/node-record-lpcm16/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/node-record-lpcm16/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uiohook-napi": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.5.tgz", + "integrity": "sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9af1581 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "d3ro-voice", + "version": "1.0.0", + "description": "로컬 AI 음성 어시스턴트", + "main": "./out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "build": "electron-vite build", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint . --ext .ts,.tsx", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", + "test": "vitest run", + "test:watch": "vitest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@types/node": "^22.13.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "electron": "^33.3.0", + "electron-vite": "^2.3.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.3.0", + "typescript": "^5.7.0", + "vite": "^5.4.0", + "vitest": "^2.1.0" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.0", + "@mui/material": "^7.0.0", + "@rollup/rollup-win32-x64-msvc": "^4.60.1", + "electron-log": "^5.2.0", + "electron-store": "^10.0.0", + "node-record-lpcm16": "^1.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "uiohook-napi": "^1.5.5" + } +} diff --git a/sidecar/main.py b/sidecar/main.py new file mode 100644 index 0000000..70051ca --- /dev/null +++ b/sidecar/main.py @@ -0,0 +1,332 @@ +""" +D3RO-VOICE STT Sidecar (FastAPI HTTP 서버) +faster-whisper를 사용한 로컬 음성 인식 서비스. + +사용법: + python main.py --port 18765 + +엔드포인트: + GET /health - 헬스체크 + POST /load - Whisper 모델 로딩 + POST /transcribe - 오디오 전사 (multipart) + POST /shutdown - 서버 종료 +""" + +from __future__ import annotations + +import argparse +import logging +import os +import signal +import sys +import time +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +import numpy as np +import uvicorn +from fastapi import FastAPI, File, Form, UploadFile +from fastapi.responses import JSONResponse + +# ── 로깅 설정 ────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + stream=sys.stdout, +) +logger = logging.getLogger("sidecar") + +# ── 전역 상태 ────────────────────────────────────────────── + +_model: "WhisperModel | None" = None +_model_id: str | None = None +_gpu_available: bool = False +_server: uvicorn.Server | None = None + +# ── FastAPI 앱 ───────────────────────────────────────────── + + +@asynccontextmanager +async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: + """서버 시작/종료 생명주기.""" + logger.info("Sidecar 서버 시작") + _detect_gpu() + yield + logger.info("Sidecar 서버 종료") + + +app = FastAPI(title="D3RO-VOICE STT Sidecar", lifespan=lifespan) + + +def _detect_gpu() -> None: + """GPU(CUDA) 사용 가능 여부를 감지한다.""" + global _gpu_available + try: + import torch + + _gpu_available = torch.cuda.is_available() + if _gpu_available: + device_name = torch.cuda.get_device_name(0) + logger.info("GPU 감지: %s", device_name) + else: + logger.info("GPU 미감지, CPU 모드로 동작") + except ImportError: + _gpu_available = False + logger.info("PyTorch 미설치, CPU 모드로 동작") + + +# ── 엔드포인트 ───────────────────────────────────────────── + + +@app.get("/health") +async def health() -> JSONResponse: + """헬스체크. sidecar가 준비되었는지 확인한다.""" + return JSONResponse( + content={ + "status": "ready" if _model is not None else "no_model", + "model": _model_id, + "gpu": _gpu_available, + } + ) + + +@app.post("/load") +async def load_model(body: dict) -> JSONResponse: # noqa: ANN001 + """Whisper 모델을 로딩한다. + + Request body: + { "model_id": "base" } -- tiny, base, small, medium, large-v3 + + Returns: + { "status": "loaded", "model_id": "base", "load_time_ms": 1234 } + """ + global _model, _model_id + + model_id: str = body.get("model_id", "base") + logger.info("모델 로딩 시작: %s", model_id) + + start_time = time.monotonic() + + try: + from faster_whisper import WhisperModel + + device = "cuda" if _gpu_available else "cpu" + compute_type = "float16" if _gpu_available else "int8" + + # 모델 크기별 compute_type 조정 + if model_id in ("large-v3", "medium") and not _gpu_available: + compute_type = "int8" + + _model = WhisperModel( + model_id, + device=device, + compute_type=compute_type, + ) + _model_id = model_id + + load_time_ms = int((time.monotonic() - start_time) * 1000) + logger.info( + "모델 로딩 완료: %s (device=%s, compute=%s, %dms)", + model_id, + device, + compute_type, + load_time_ms, + ) + + return JSONResponse( + content={ + "status": "loaded", + "model_id": model_id, + "load_time_ms": load_time_ms, + } + ) + + except Exception as exc: + logger.error("모델 로딩 실패: %s", exc) + return JSONResponse( + status_code=500, + content={"status": "error", "message": str(exc)}, + ) + + +@app.post("/transcribe") +async def transcribe( + audio: UploadFile = File(...), + language: str = Form("auto"), + vad_filter: str = Form("true"), + initial_prompt: str = Form(""), +) -> JSONResponse: + """오디오 파일을 전사한다. + + Multipart form: + audio - PCM16 16kHz mono 바이너리 파일 + language - 언어 코드 ('auto', 'ko', 'en', ...) + vad_filter - VAD 필터 활성화 ('true' / 'false') + initial_prompt - 초기 프롬프트 (컨텍스트 힌트) + + Returns: + { + "text": "전사된 텍스트", + "segments": [...], + "language": "ko", + "duration": 3.5, + "processing_time": 1234 + } + """ + if _model is None: + return JSONResponse( + status_code=503, + content={"status": "error", "message": "모델이 로딩되지 않았습니다"}, + ) + + start_time = time.monotonic() + + try: + # PCM16 바이너리 읽기 + pcm_bytes = await audio.read() + + if len(pcm_bytes) == 0: + return JSONResponse( + status_code=400, + content={"status": "error", "message": "오디오 데이터가 비어있습니다"}, + ) + + # PCM16 → float32 변환 (-1.0 ~ 1.0) + audio_array = ( + np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32) / 32768.0 + ) + + # 오디오 길이 계산 (16kHz mono 기준) + sample_rate = 16000 + audio_duration = len(audio_array) / sample_rate + + logger.info( + "전사 시작: %.1f초 오디오, language=%s, vad=%s", + audio_duration, + language, + vad_filter, + ) + + # 전사 옵션 구성 + transcribe_kwargs: dict = { + "vad_filter": vad_filter.lower() == "true", + "beam_size": 5, + } + + if language != "auto": + transcribe_kwargs["language"] = language + + if initial_prompt: + transcribe_kwargs["initial_prompt"] = initial_prompt + + # faster-whisper 전사 실행 + segments_iter, info = _model.transcribe(audio_array, **transcribe_kwargs) + + # 세그먼트 수집 + segments_list: list[dict] = [] + full_text_parts: list[str] = [] + + for segment in segments_iter: + seg_dict = { + "text": segment.text.strip(), + "start": round(segment.start, 3), + "end": round(segment.end, 3), + "avg_logprob": round(segment.avg_logprob, 4), + } + segments_list.append(seg_dict) + full_text_parts.append(segment.text.strip()) + + full_text = " ".join(full_text_parts).strip() + processing_time = int((time.monotonic() - start_time) * 1000) + + detected_language = info.language if info.language else "unknown" + + logger.info( + "전사 완료: '%s' (lang=%s, %.1f초, %dms)", + full_text[:80], + detected_language, + audio_duration, + processing_time, + ) + + return JSONResponse( + content={ + "text": full_text, + "segments": segments_list, + "language": detected_language, + "duration": round(audio_duration, 3), + "processing_time": processing_time, + } + ) + + except Exception as exc: + logger.error("전사 실패: %s", exc, exc_info=True) + return JSONResponse( + status_code=500, + content={"status": "error", "message": str(exc)}, + ) + + +@app.post("/shutdown") +async def shutdown() -> JSONResponse: + """서버를 graceful하게 종료한다.""" + logger.info("종료 요청 수신") + + if _server is not None: + _server.should_exit = True + + return JSONResponse(content={"status": "shutting_down"}) + + +# ── 메인 ─────────────────────────────────────────────────── + + +def main() -> None: + """CLI 진입점.""" + global _server + + parser = argparse.ArgumentParser(description="D3RO-VOICE STT Sidecar") + parser.add_argument( + "--port", + type=int, + default=18765, + help="HTTP 서버 포트 (기본: 18765)", + ) + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + help="HTTP 서버 호스트 (기본: 127.0.0.1)", + ) + args = parser.parse_args() + + logger.info("D3RO-VOICE STT Sidecar 시작 (port=%d)", args.port) + + # SIGINT/SIGTERM 핸들러 + def signal_handler(signum: int, _frame: object) -> None: + sig_name = signal.Signals(signum).name + logger.info("시그널 수신: %s, 종료 시작", sig_name) + if _server is not None: + _server.should_exit = True + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # uvicorn 서버 구성 및 시작 + config = uvicorn.Config( + app=app, + host=args.host, + port=args.port, + log_level="warning", # uvicorn 자체 로그는 최소화 (우리 로거 사용) + access_log=False, + ) + _server = uvicorn.Server(config) + _server.run() + + logger.info("Sidecar 서버 종료 완료") + + +if __name__ == "__main__": + main() diff --git a/sidecar/requirements.txt b/sidecar/requirements.txt new file mode 100644 index 0000000..afb99ca --- /dev/null +++ b/sidecar/requirements.txt @@ -0,0 +1,5 @@ +faster-whisper>=1.0.0 +fastapi>=0.109.0 +uvicorn>=0.27.0 +python-multipart>=0.0.6 +numpy>=1.24.0 diff --git a/src/main/bootstrap.ts b/src/main/bootstrap.ts new file mode 100644 index 0000000..88d556e --- /dev/null +++ b/src/main/bootstrap.ts @@ -0,0 +1,78 @@ +// src/main/bootstrap.ts — 초기화 시퀀스 + +import { app, dialog } from 'electron' +import { initLoggerService, getLogger } from './services/LoggerService' +import { initConfigService } from './services/ConfigService' +import { getHotkeyService } from './services/HotkeyService' +import { getVoiceModeService } from './services/VoiceModeService' +import { createMainWindow } from './windows/WindowManager' +import { createTray } from './windows/TrayManager' +import { registerAllIpcHandlers } from './ipc' + +const logger = getLogger('bootstrap') + +interface BootstrapStep { + name: string + critical: boolean + fn: () => Promise +} + +export async function bootstrap(): Promise { + const steps: BootstrapStep[] = [ + { name: 'logger', critical: false, fn: initLogger }, + { name: 'config', critical: false, fn: initConfig }, + { name: 'create-windows', critical: true, fn: createWindows }, + { name: 'tray', critical: false, fn: initTray }, + { name: 'ipc-handlers', critical: true, fn: initIpcHandlers }, + { name: 'hotkey', critical: false, fn: initHotkey }, + { name: 'voice-mode', critical: false, fn: initVoiceMode } + ] + + for (const step of steps) { + try { + await step.fn() + logger.info(`[bootstrap] ${step.name} initialized`) + } catch (error) { + logger.error(`[bootstrap] ${step.name} failed:`, error) + if (step.critical) { + dialog.showErrorBox( + 'D3RO-VOICE 초기화 실패', + `${step.name}: ${error instanceof Error ? error.message : String(error)}` + ) + app.quit() + return + } + } + } +} + +async function initLogger(): Promise { + initLoggerService() +} + +async function initConfig(): Promise { + initConfigService() +} + +async function createWindows(): Promise { + createMainWindow() +} + +async function initTray(): Promise { + createTray() +} + +async function initIpcHandlers(): Promise { + registerAllIpcHandlers() +} + +async function initHotkey(): Promise { + const hotkey = getHotkeyService() + hotkey.loadFromConfig() + hotkey.start() +} + +async function initVoiceMode(): Promise { + const voiceMode = getVoiceModeService() + voiceMode.connectHotkey() +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..f185d39 --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,34 @@ +// src/main/index.ts — 앱 진입점 + +import { app } from 'electron' +import { bootstrap } from './bootstrap' +import { setupLifecycle } from './lifecycle' +import { getMainWindow } from './windows/WindowManager' + +// 단일 인스턴스 잠금 +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} else { + app.on('second-instance', () => { + // 기존 인스턴스의 메인 윈도우를 활성화 + const mainWindow = getMainWindow() + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) + + app.whenReady().then(async () => { + await bootstrap() + setupLifecycle() + }) + + app.on('window-all-closed', () => { + // Windows에서는 모든 윈도우 닫아도 앱 유지 (트레이) + if (process.platform !== 'darwin') { + // closeToTray 설정 확인은 lifecycle에서 처리 + } + }) +} diff --git a/src/main/ipc/audio-handlers.ts b/src/main/ipc/audio-handlers.ts new file mode 100644 index 0000000..102478e --- /dev/null +++ b/src/main/ipc/audio-handlers.ts @@ -0,0 +1,37 @@ +// src/main/ipc/audio-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getAudioCaptureService } from '../services/AudioCaptureService' +import { configGet, configSet } from '../services/ConfigService' +import type { SetDeviceParams } from '@shared/types' + +export function registerAudioHandlers(): void { + ipcMain.handle(IPC_CHANNELS.AUDIO.GET_DEVICES, async () => { + try { + const devices = await getAudioCaptureService().getDevices() + return ipcSuccess(devices) + } catch { + return ipcError(ErrorCode.AudioDeviceNotFound, 'Failed to enumerate audio devices') + } + }) + + ipcMain.handle(IPC_CHANNELS.AUDIO.GET_SELECTED_DEVICE, async () => { + return ipcSuccess(configGet('selectedDeviceId')) + }) + + ipcMain.handle(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, async (_event, params: SetDeviceParams) => { + try { + configSet('selectedDeviceId', params.deviceId) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.AudioDeviceNotFound, 'Failed to set device') + } + }) + + ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => { + // Phase 1: 스텁 + return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: false }) + }) +} diff --git a/src/main/ipc/config-handlers.ts b/src/main/ipc/config-handlers.ts new file mode 100644 index 0000000..b58f6ea --- /dev/null +++ b/src/main/ipc/config-handlers.ts @@ -0,0 +1,72 @@ +// src/main/ipc/config-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { configGet, configSet, configGetAll, configReset } from '../services/ConfigService' +import type { + ConfigGetParams, + ConfigSetParams, + ConfigResetParams, + SetThemeParams, + SetLanguageParams, + AppConfig +} from '@shared/types' + +export function registerConfigHandlers(): void { + ipcMain.handle(IPC_CHANNELS.CONFIG.GET, async (_event, params: ConfigGetParams) => { + try { + return ipcSuccess(configGet(params.key)) + } catch { + return ipcError(ErrorCode.ConfigReadFailed, `Failed to read config key: ${params.key}`) + } + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.SET, async (_event, params: ConfigSetParams) => { + try { + configSet(params.key, params.value as AppConfig[typeof params.key]) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.ConfigWriteFailed, `Failed to write config key: ${params.key}`) + } + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.GET_ALL, async () => { + return ipcSuccess(configGetAll()) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.RESET, async (_event, params: ConfigResetParams) => { + try { + configReset(params.key) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.ConfigResetFailed, 'Failed to reset config') + } + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.GET_THEME, async () => { + return ipcSuccess(configGet('theme')) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.SET_THEME, async (_event, params: SetThemeParams) => { + configSet('theme', params.theme) + return ipcSuccess(undefined) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.GET_LANGUAGE, async () => { + return ipcSuccess(configGet('language')) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.SET_LANGUAGE, async (_event, params: SetLanguageParams) => { + configSet('language', params.language) + return ipcSuccess(undefined) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.GET_AUTO_LAUNCH, async () => { + return ipcSuccess(configGet('autoLaunch')) + }) + + ipcMain.handle(IPC_CHANNELS.CONFIG.GET_CLOSE_TO_TRAY, async () => { + return ipcSuccess(configGet('closeToTray')) + }) +} diff --git a/src/main/ipc/hotkey-handlers.ts b/src/main/ipc/hotkey-handlers.ts new file mode 100644 index 0000000..9216242 --- /dev/null +++ b/src/main/ipc/hotkey-handlers.ts @@ -0,0 +1,67 @@ +// src/main/ipc/hotkey-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getHotkeyService } from '../services/HotkeyService' +import { configGet, configSet } from '../services/ConfigService' +import type { SetHotkeyParams, SetEnabledParams } from '@shared/types' + +export function registerHotkeyHandlers(): void { + ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_DICTATION_SHORTCUT, async () => { + return ipcSuccess(configGet('dictationShortcut')) + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_DICTATION_SHORTCUT, async (_event, params: SetHotkeyParams) => { + try { + configSet('dictationShortcut', params.binding) + getHotkeyService().loadFromConfig() + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set dictation shortcut') + } + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_HANDS_FREE_SHORTCUT, async () => { + return ipcSuccess(configGet('handsFreeShortcut')) + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_HANDS_FREE_SHORTCUT, async (_event, params: SetHotkeyParams) => { + try { + configSet('handsFreeShortcut', params.binding) + getHotkeyService().loadFromConfig() + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set hands-free shortcut') + } + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_COMMAND_SHORTCUT, async () => { + return ipcSuccess(configGet('commandShortcut')) + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_COMMAND_SHORTCUT, async (_event, params: SetHotkeyParams) => { + try { + configSet('commandShortcut', params.binding) + getHotkeyService().loadFromConfig() + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set command shortcut') + } + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.IS_ENABLED, async () => { + return ipcSuccess(configGet('hotkeyEnabled')) + }) + + ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_ENABLED, async (_event, params: SetEnabledParams) => { + configSet('hotkeyEnabled', params.enabled) + const hotkey = getHotkeyService() + if (params.enabled) { + hotkey.start() + } else { + hotkey.stop() + } + return ipcSuccess(undefined) + }) +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts new file mode 100644 index 0000000..0cc962c --- /dev/null +++ b/src/main/ipc/index.ts @@ -0,0 +1,23 @@ +// src/main/ipc/index.ts — IPC 핸들러 일괄 등록 + +import { registerAudioHandlers } from './audio-handlers' +import { registerConfigHandlers } from './config-handlers' +import { registerWindowHandlers } from './window-handlers' +import { registerSystemHandlers } from './system-handlers' +import { registerVoiceHandlers } from './voice-handlers' +import { registerSTTHandlers } from './stt-handlers' +import { registerHotkeyHandlers } from './hotkey-handlers' +import { getLogger } from '../services/LoggerService' + +const logger = getLogger('ipc') + +export function registerAllIpcHandlers(): void { + registerAudioHandlers() + registerConfigHandlers() + registerWindowHandlers() + registerSystemHandlers() + registerVoiceHandlers() + registerSTTHandlers() + registerHotkeyHandlers() + logger.info('All IPC handlers registered') +} diff --git a/src/main/ipc/stt-handlers.ts b/src/main/ipc/stt-handlers.ts new file mode 100644 index 0000000..324882d --- /dev/null +++ b/src/main/ipc/stt-handlers.ts @@ -0,0 +1,52 @@ +// src/main/ipc/stt-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getLocalSTTService } from '../services/LocalSTTService' +import { configGet, configSet } from '../services/ConfigService' +import type { SetSTTModelParams, SetSTTLanguageParams } from '@shared/types' + +export function registerSTTHandlers(): void { + ipcMain.handle(IPC_CHANNELS.STT.GET_STATUS, async () => { + try { + const stt = getLocalSTTService() + return ipcSuccess(stt.getStatus()) + } catch { + return ipcError(ErrorCode.STTSidecarCommunicationFailed, 'Failed to get STT status') + } + }) + + ipcMain.handle(IPC_CHANNELS.STT.GET_MODELS, async () => { + try { + const stt = getLocalSTTService() + return ipcSuccess(await stt.getModels()) + } catch { + return ipcError(ErrorCode.STTModelNotFound, 'Failed to get models') + } + }) + + ipcMain.handle(IPC_CHANNELS.STT.GET_ACTIVE_MODEL, async () => { + return ipcSuccess(configGet('sttModelId')) + }) + + ipcMain.handle(IPC_CHANNELS.STT.SET_MODEL, async (_event, params: SetSTTModelParams) => { + try { + configSet('sttModelId', params.modelId) + const stt = getLocalSTTService() + await stt.initialize(params.modelId) + return ipcSuccess(undefined) + } catch { + return ipcError(ErrorCode.STTModelLoadFailed, 'Failed to set STT model') + } + }) + + ipcMain.handle(IPC_CHANNELS.STT.GET_LANGUAGE, async () => { + return ipcSuccess(configGet('sttLanguage')) + }) + + ipcMain.handle(IPC_CHANNELS.STT.SET_LANGUAGE, async (_event, params: SetSTTLanguageParams) => { + configSet('sttLanguage', params.language) + return ipcSuccess(undefined) + }) +} diff --git a/src/main/ipc/system-handlers.ts b/src/main/ipc/system-handlers.ts new file mode 100644 index 0000000..2e97141 --- /dev/null +++ b/src/main/ipc/system-handlers.ts @@ -0,0 +1,28 @@ +// src/main/ipc/system-handlers.ts + +import { ipcMain, app, systemPreferences } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess } from '@shared/errors' +import type { PermissionStatus } from '@shared/types' + +export function registerSystemHandlers(): void { + ipcMain.handle(IPC_CHANNELS.SYSTEM.GET_PLATFORM, async () => { + return ipcSuccess(process.platform) + }) + + ipcMain.handle(IPC_CHANNELS.SYSTEM.GET_VERSION, async () => { + return ipcSuccess(app.getVersion()) + }) + + ipcMain.handle(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION, async () => { + let status: PermissionStatus = 'unknown' + if (process.platform === 'darwin') { + const macStatus = systemPreferences.getMediaAccessStatus('microphone') + status = macStatus === 'granted' ? 'granted' : macStatus === 'denied' ? 'denied' : 'unknown' + } else { + // Windows: 마이크 권한은 별도 체크 불필요 (시스템 설정에서 관리) + status = 'granted' + } + return ipcSuccess(status) + }) +} diff --git a/src/main/ipc/voice-handlers.ts b/src/main/ipc/voice-handlers.ts new file mode 100644 index 0000000..cac9f15 --- /dev/null +++ b/src/main/ipc/voice-handlers.ts @@ -0,0 +1,53 @@ +// src/main/ipc/voice-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors' +import { getVoiceModeService } from '../services/VoiceModeService' +import type { StartRecordingParams, StopRecordingParams, CancelRecordingParams, SetVoiceModeParams } from '@shared/types' + +export function registerVoiceHandlers(): void { + ipcMain.handle(IPC_CHANNELS.VOICE.START_RECORDING, async (_event, params: StartRecordingParams) => { + try { + const voice = getVoiceModeService() + await voice.startSession('dictation') + const session = voice.currentSession + return ipcSuccess({ sessionId: session?.id ?? params.sessionId ?? '' }) + } catch { + return ipcError(ErrorCode.AudioCaptureStartFailed, 'Failed to start recording') + } + }) + + ipcMain.handle(IPC_CHANNELS.VOICE.STOP_RECORDING, async (_event, params: StopRecordingParams) => { + try { + const voice = getVoiceModeService() + await voice.stopSession() + return ipcSuccess({ + sessionId: params.sessionId, + text: voice.currentSession?.transcription ?? '', + durationMs: voice.currentSession ? Date.now() - voice.currentSession.startedAt : 0 + }) + } catch { + return ipcError(ErrorCode.STTTranscriptionFailed, 'Failed to stop recording') + } + }) + + ipcMain.handle(IPC_CHANNELS.VOICE.CANCEL_RECORDING, async (_event, _params: CancelRecordingParams) => { + getVoiceModeService().cancelSession() + return ipcSuccess(undefined) + }) + + ipcMain.handle(IPC_CHANNELS.VOICE.GET_STATE, async () => { + return ipcSuccess(getVoiceModeService().getState()) + }) + + ipcMain.handle(IPC_CHANNELS.VOICE.SET_MODE, async (_event, params: SetVoiceModeParams) => { + // Phase 2: 모드만 설정에 저장 (실제 모드 전환은 핫키에서 처리) + return ipcSuccess(undefined) + }) + + ipcMain.handle(IPC_CHANNELS.VOICE.GET_MODE, async () => { + const voice = getVoiceModeService() + return ipcSuccess(voice.currentSession?.mode ?? 'dictation') + }) +} diff --git a/src/main/ipc/window-handlers.ts b/src/main/ipc/window-handlers.ts new file mode 100644 index 0000000..602dd04 --- /dev/null +++ b/src/main/ipc/window-handlers.ts @@ -0,0 +1,31 @@ +// src/main/ipc/window-handlers.ts + +import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import { ipcSuccess } from '@shared/errors' +import { getMainWindow } from '../windows/WindowManager' + +export function registerWindowHandlers(): void { + ipcMain.on(IPC_CHANNELS.WINDOW.MINIMIZE, () => { + getMainWindow()?.minimize() + }) + + ipcMain.on(IPC_CHANNELS.WINDOW.MAXIMIZE, () => { + const win = getMainWindow() + if (win) { + if (win.isMaximized()) { + win.unmaximize() + } else { + win.maximize() + } + } + }) + + ipcMain.on(IPC_CHANNELS.WINDOW.CLOSE, () => { + getMainWindow()?.close() + }) + + ipcMain.handle(IPC_CHANNELS.WINDOW.IS_MAXIMIZED, async () => { + return ipcSuccess(getMainWindow()?.isMaximized() ?? false) + }) +} diff --git a/src/main/lifecycle.ts b/src/main/lifecycle.ts new file mode 100644 index 0000000..5486528 --- /dev/null +++ b/src/main/lifecycle.ts @@ -0,0 +1,37 @@ +// src/main/lifecycle.ts — 종료 시퀀스 + +import { app } from 'electron' +import { getLogger } from './services/LoggerService' +import { getConfigService } from './services/ConfigService' + +const logger = getLogger('lifecycle') + +let isQuitting = false + +export function getIsQuitting(): boolean { + return isQuitting +} + +export function setIsQuitting(value: boolean): void { + isQuitting = value +} + +export function setupLifecycle(): void { + app.on('before-quit', () => { + logger.info('[lifecycle] before-quit') + isQuitting = true + }) + + app.on('will-quit', () => { + logger.info('[lifecycle] will-quit — flushing config') + try { + const configService = getConfigService() + if (configService) { + // electron-store는 동기적으로 디스크에 쓰므로 별도 flush 불필요 + logger.info('[lifecycle] config flush complete') + } + } catch (error) { + logger.warn('[lifecycle] config flush failed:', error) + } + }) +} diff --git a/src/main/services/AudioCaptureService.ts b/src/main/services/AudioCaptureService.ts new file mode 100644 index 0000000..fd506fc --- /dev/null +++ b/src/main/services/AudioCaptureService.ts @@ -0,0 +1,365 @@ +// src/main/services/AudioCaptureService.ts +// 마이크 PCM 캡처 서비스. 설계서 01의 IAudioCaptureService 구현. +// node-record-lpcm16 + SoX로 PCM16 16kHz mono 캡처. + +import { EventEmitter } from 'events' +import { record } from 'node-record-lpcm16' +import type { Recording } from 'node-record-lpcm16' +import type { Readable } from 'stream' +import { getLogger } from './LoggerService' +import { configGet } from './ConfigService' +import type { AudioDevice } from '@shared/types' +import { AUDIO_FORMAT, TIMING } from '@shared/constants' +import { D3ROError, ErrorCode } from '@shared/errors' + +const logger = getLogger('AudioCaptureService') + +/** 60ms 프레임 크기 (바이트): 16000 * 2 * 0.06 = 1920 */ +const FRAME_SIZE_BYTES = (AUDIO_FORMAT.SAMPLE_RATE * AUDIO_FORMAT.BYTES_PER_SAMPLE * 60) / 1000 + +export type CaptureState = 'idle' | 'starting' | 'capturing' | 'stopping' | 'error' + +interface AudioCaptureEvents { + 'audio-data': (payload: { buffer: Buffer; timestamp: number }) => void + 'audio-level': (payload: { level: number; timestamp: number }) => void + 'device-changed': (payload: { + previous: AudioDevice | null + current: AudioDevice + }) => void + started: (payload: { deviceId: string }) => void + stopped: (payload: { reason: 'manual' | 'device-lost' | 'error' }) => void + error: (payload: { error: D3ROError }) => void +} + +/** + * PCM16 버퍼에서 RMS(Root Mean Square) 오디오 레벨을 계산한다. + * 반환값은 0.0 ~ 1.0 범위로 정규화된다. + */ +function calculateRMS(buffer: Buffer): number { + const samples = buffer.length / AUDIO_FORMAT.BYTES_PER_SAMPLE + if (samples === 0) return 0 + + let sumSquares = 0 + for (let i = 0; i < buffer.length; i += AUDIO_FORMAT.BYTES_PER_SAMPLE) { + const sample = buffer.readInt16LE(i) + sumSquares += sample * sample + } + + const rms = Math.sqrt(sumSquares / samples) + // PCM16 최대값 32768로 나눠서 0.0~1.0 범위로 정규화 + return Math.min(1.0, rms / 32768) +} + +class AudioCaptureService extends EventEmitter { + private _state: CaptureState = 'idle' + private _currentDevice: AudioDevice | null = null + private _refCount = 0 + private _recording: Recording | null = null + private _stream: Readable | null = null + private _levelInterval: ReturnType | null = null + + /** 프레임 조립용 잔여 바이트 버퍼 */ + private _residualBuffer: Buffer = Buffer.alloc(0) + + /** RMS 계산용 누적 버퍼 (100ms 간격 emit) */ + private _levelAccumulator: Buffer[] = [] + + get state(): CaptureState { + return this._state + } + + get currentDevice(): AudioDevice | null { + return this._currentDevice + } + + async start(deviceId?: string): Promise { + if (this._state === 'capturing') { + this._refCount++ + logger.debug(`Reference count increased to ${this._refCount}`) + return + } + + if (this._state !== 'idle' && this._state !== 'error') { + logger.warn(`Cannot start capture in state: ${this._state}`) + return + } + + this._state = 'starting' + const selectedDeviceId = deviceId ?? configGet('selectedDeviceId') + + try { + logger.info( + `Starting audio capture (device: ${selectedDeviceId ?? 'default'}, ` + + `format: ${AUDIO_FORMAT.SAMPLE_RATE}Hz ${AUDIO_FORMAT.CHANNELS}ch ${AUDIO_FORMAT.BIT_DEPTH}bit)` + ) + + // node-record-lpcm16 으로 SoX rec 프로세스 spawn + const recordingOptions: Record = { + sampleRate: AUDIO_FORMAT.SAMPLE_RATE, + channels: AUDIO_FORMAT.CHANNELS, + recorder: 'sox', + audioType: 'raw', // 헤더 없는 PCM raw 출력 + endOnSilence: false + } + + // 특정 디바이스가 지정된 경우 AUDIODEV 환경변수로 전달 + if (selectedDeviceId && selectedDeviceId !== 'default') { + recordingOptions.device = selectedDeviceId + } + + this._recording = record(recordingOptions) + this._stream = this._recording.stream() + this._residualBuffer = Buffer.alloc(0) + this._levelAccumulator = [] + + // 스트림 데이터 수신: 60ms 프레임 단위로 잘라 emit + this._stream.on('data', (chunk: Buffer) => { + this._onAudioChunk(chunk) + }) + + // 스트림 에러 처리 + this._stream.on('error', (errorMessage: string | Error) => { + const msg = typeof errorMessage === 'string' ? errorMessage : errorMessage.message + logger.error(`Audio stream error: ${msg}`) + + // SoX 미설치 감지 + const isSoxMissing = + msg.includes('ENOENT') || + msg.includes('not found') || + msg.includes('is not recognized') + + const errorCode = isSoxMissing + ? ErrorCode.AudioCaptureStartFailed + : ErrorCode.AudioStreamError + + const d3roError = new D3ROError( + errorCode, + isSoxMissing + ? 'SoX가 설치되어 있지 않거나 PATH에 없습니다. SoX를 설치해주세요: https://sox.sourceforge.net' + : `Audio stream error: ${msg}` + ) + + this._handleError(d3roError, 'error') + }) + + // SoX 프로세스 종료 감지 + this._stream.on('end', () => { + if (this._state === 'capturing') { + logger.warn('Audio stream ended unexpectedly') + this._handleError( + new D3ROError(ErrorCode.AudioCaptureFailed, 'Audio capture process terminated unexpectedly'), + 'device-lost' + ) + } + }) + + // SoX child process 에러 이벤트 (spawn 실패 등) + if (this._recording.process) { + this._recording.process.on('error', (err: Error) => { + logger.error(`SoX process spawn error: ${err.message}`) + + const d3roError = new D3ROError( + ErrorCode.AudioCaptureStartFailed, + `SoX 프로세스 시작 실패: ${err.message}. SoX가 설치되어 있는지 확인해주세요.` + ) + + this._handleError(d3roError, 'error') + }) + } + + this._currentDevice = { + deviceId: selectedDeviceId ?? 'default', + label: 'Default Microphone', + isDefault: !selectedDeviceId || selectedDeviceId === 'default' + } + + this._state = 'capturing' + this._refCount = 1 + + // 100ms 간격으로 audio-level 이벤트 emit + this._levelInterval = setInterval(() => { + this._emitAudioLevel() + }, TIMING.AUDIO_LEVEL_INTERVAL) + + this.emit('started', { deviceId: this._currentDevice.deviceId }) + logger.info('Audio capture started') + } catch (error) { + this._state = 'error' + const d3roError = new D3ROError( + ErrorCode.AudioCaptureStartFailed, + `Failed to start audio capture: ${error instanceof Error ? error.message : String(error)}` + ) + this.emit('error', { error: d3roError }) + throw d3roError + } + } + + async stop(): Promise { + if (this._state !== 'capturing') { + return + } + + this._refCount-- + if (this._refCount > 0) { + logger.debug(`Reference count decreased to ${this._refCount}`) + return + } + + this._state = 'stopping' + this._cleanup() + this._state = 'idle' + this._currentDevice = null + this.emit('stopped', { reason: 'manual' }) + logger.info('Audio capture stopped') + } + + async getDevices(): Promise { + // 현재 Phase에서는 기본 디바이스만 반환. 실제 디바이스 열거는 추후. + logger.debug('Getting audio devices (default only)') + return [ + { + deviceId: 'default', + label: 'Default Microphone', + isDefault: true + } + ] + } + + getCurrentDevice(): AudioDevice | null { + return this._currentDevice + } + + dispose(): void { + this._cleanup() + this._state = 'idle' + this._currentDevice = null + this._refCount = 0 + this.removeAllListeners() + logger.info('AudioCaptureService disposed') + } + + /** + * 수신된 오디오 청크를 60ms 프레임(1920 bytes) 단위로 분할하여 emit한다. + * 잔여 바이트는 다음 청크와 결합한다. + */ + private _onAudioChunk(chunk: Buffer): void { + // 잔여 버퍼와 새 청크를 결합 + const combined = this._residualBuffer.length > 0 + ? Buffer.concat([this._residualBuffer, chunk]) + : chunk + + let offset = 0 + + // 60ms 프레임 단위로 분할하여 emit + while (offset + FRAME_SIZE_BYTES <= combined.length) { + const frame = combined.subarray(offset, offset + FRAME_SIZE_BYTES) + offset += FRAME_SIZE_BYTES + + this.emit('audio-data', { + buffer: Buffer.from(frame), // 방어적 복사 + timestamp: Date.now() + }) + + // RMS 계산용 누적 + this._levelAccumulator.push(frame) + } + + // 남은 바이트는 잔여 버퍼에 보관 + if (offset < combined.length) { + this._residualBuffer = Buffer.from(combined.subarray(offset)) + } else { + this._residualBuffer = Buffer.alloc(0) + } + } + + /** + * 100ms 간격으로 누적된 오디오 데이터의 RMS 레벨을 계산하여 emit한다. + */ + private _emitAudioLevel(): void { + if (this._levelAccumulator.length === 0) { + this.emit('audio-level', { level: 0, timestamp: Date.now() }) + return + } + + // 누적된 프레임들을 하나로 합쳐서 RMS 계산 + const combined = Buffer.concat(this._levelAccumulator) + this._levelAccumulator = [] + + const level = calculateRMS(combined) + this.emit('audio-level', { level, timestamp: Date.now() }) + } + + /** + * 에러 발생 시 리소스 정리 + 에러 이벤트 emit. + */ + private _handleError(error: D3ROError, stopReason: 'device-lost' | 'error'): void { + if (this._state === 'idle' || this._state === 'stopping') { + return + } + + this._cleanup() + this._state = 'error' + this._currentDevice = null + this.emit('error', { error }) + this.emit('stopped', { reason: stopReason }) + } + + /** + * 녹음 프로세스와 타이머를 정리한다. + */ + private _cleanup(): void { + if (this._levelInterval) { + clearInterval(this._levelInterval) + this._levelInterval = null + } + + if (this._stream) { + this._stream.removeAllListeners() + this._stream = null + } + + if (this._recording) { + try { + this._recording.stop() + } catch { + // 이미 종료된 프로세스 kill 시 에러 무시 + } + this._recording = null + } + + this._residualBuffer = Buffer.alloc(0) + this._levelAccumulator = [] + } + + // EventEmitter 타입 오버라이드 + override on( + event: K, + listener: AudioCaptureEvents[K] + ): this { + return super.on(event, listener) + } + + override off( + event: K, + listener: AudioCaptureEvents[K] + ): this { + return super.off(event, listener) + } + + override emit( + event: K, + ...args: Parameters + ): boolean { + return super.emit(event, ...args) + } +} + +// 싱글톤 +let instance: AudioCaptureService | null = null + +export function getAudioCaptureService(): AudioCaptureService { + if (!instance) { + instance = new AudioCaptureService() + } + return instance +} diff --git a/src/main/services/ConfigService.ts b/src/main/services/ConfigService.ts new file mode 100644 index 0000000..5f7e8a5 --- /dev/null +++ b/src/main/services/ConfigService.ts @@ -0,0 +1,119 @@ +// src/main/services/ConfigService.ts +// electron-store 기반 설정 관리. 설계서 02의 AppConfig 타입 사용. + +import { EventEmitter } from 'events' +import type { AppConfig, ConfigChangedEvent } from '@shared/types' +import { getLogger } from './LoggerService' + +const logger = getLogger('ConfigService') + +// electron-store v10은 ESM 전용이므로 동적 import 필요 +interface ElectronStore> { + get(key: K): T[K] + set(key: K, value: T[K]): void + store: T +} + +const CONFIG_DEFAULTS: AppConfig = { + theme: 'auto', + language: 'ko', + closeToTray: true, + autoLaunch: false, + soundEnabled: true, + selectedDeviceId: null, + sttModelId: 'base', + sttLanguage: 'auto', + ttsVoiceId: null, + ttsSpeed: 1.0, + ollamaServerUrl: 'http://localhost:11434', + llmModelId: null, + defaultLLMAction: 'refine', + dictationShortcut: { + keyCode: 0xa5, // Right Alt + ctrl: false, + alt: false, + shift: false, + meta: false, + displayLabel: 'Right Alt' + }, + handsFreeShortcut: { + keyCode: 0xa5, + ctrl: false, + alt: false, + shift: false, + meta: false, + displayLabel: 'Right Alt (double)' + }, + commandShortcut: { + keyCode: 0xa5, + ctrl: true, + alt: false, + shift: false, + meta: false, + displayLabel: 'Ctrl + Right Alt' + }, + hotkeyEnabled: true, + insertMethod: 'clipboard', + autoInsert: true, + maxHistoryEntries: 1000 +} + +let store: ElectronStore | null = null +const emitter = new EventEmitter() + +export async function initConfigService(): Promise { + const { default: Store } = await import('electron-store') + store = new Store({ + name: 'd3ro-voice-config', + defaults: CONFIG_DEFAULTS + }) + logger.info('ConfigService initialized') +} + +export function getConfigService(): ElectronStore | null { + return store +} + +export function configGet(key: K): AppConfig[K] { + if (!store) { + logger.warn(`ConfigService not initialized, returning default for "${key}"`) + return CONFIG_DEFAULTS[key] + } + return store.get(key) +} + +export function configSet(key: K, value: AppConfig[K]): void { + if (!store) { + logger.warn(`ConfigService not initialized, cannot set "${key}"`) + return + } + const previousValue = store.get(key) + store.set(key, value) + + const event: ConfigChangedEvent = { + key, + value, + previousValue + } + emitter.emit('config-changed', event) + logger.debug(`Config changed: ${key}`) +} + +export function configGetAll(): AppConfig { + if (!store) return { ...CONFIG_DEFAULTS } + return store.store +} + +export function configReset(key?: keyof AppConfig): void { + if (!store) return + if (key) { + store.set(key, CONFIG_DEFAULTS[key]) + } else { + store.store = { ...CONFIG_DEFAULTS } + } +} + +export function onConfigChanged(callback: (event: ConfigChangedEvent) => void): () => void { + emitter.on('config-changed', callback) + return () => emitter.off('config-changed', callback) +} diff --git a/src/main/services/HotkeyService.ts b/src/main/services/HotkeyService.ts new file mode 100644 index 0000000..9f7cfa4 --- /dev/null +++ b/src/main/services/HotkeyService.ts @@ -0,0 +1,536 @@ +// src/main/services/HotkeyService.ts +// uiohook-napi 기반 글로벌 키보드 후킹 서비스. +// 설계서 01의 IHotkeyService 구현. Speakly HotkeyService + HotkeyConfig 패턴 참조. + +import { EventEmitter } from 'events' +import { uIOhook, UiohookKey } from 'uiohook-napi' +import type { UiohookKeyboardEvent } from 'uiohook-napi' +import { getLogger } from './LoggerService' +import { configGet } from './ConfigService' +import { D3ROError, ErrorCode } from '@shared/errors' +import { TIMING } from '@shared/constants' +import type { HotkeyBinding } from '@shared/types' + +const logger = getLogger('HotkeyService') + +// ============================================================ +// 내부 타입 +// ============================================================ + +export interface HotkeyConfig { + /** 핫키 식별자 (예: 'voice-dictation', 'voice-handsfree') */ + id: string + /** uiohook 키코드 */ + keyCode: number + /** 수정자 키 목록 */ + modifiers: HotkeyModifier[] + /** true=hold-to-talk (누르고 있는 동안 활성), false=toggle */ + holdMode: boolean + /** 더블프레스 감지 활성화 여부 */ + doublePressEnabled: boolean + /** 활성화 여부 */ + enabled: boolean +} + +export type HotkeyModifier = 'ctrl' | 'alt' | 'shift' | 'meta' + +interface HotkeyServiceEvents { + 'hotkey-pressed': (payload: { config: HotkeyConfig; timestamp: number }) => void + 'hotkey-released': (payload: { + config: HotkeyConfig + durationMs: number + timestamp: number + }) => void + 'double-press': (payload: { + config: HotkeyConfig + intervalMs: number + timestamp: number + }) => void + error: (payload: { error: D3ROError }) => void +} + +// ============================================================ +// Windows VK 코드 → uiohook 키코드 매핑 +// ============================================================ + +/** + * Windows Virtual-Key 코드를 uiohook-napi 키코드로 변환한다. + * ConfigService에 저장된 HotkeyBinding.keyCode는 Windows VK 코드이므로 + * uiohook 이벤트와 비교하려면 변환이 필요하다. + */ +const VK_TO_UIOHOOK: ReadonlyMap = new Map([ + // 수정자 키 + [0xa0, UiohookKey.Shift], // VK_LSHIFT + [0xa1, UiohookKey.ShiftRight], // VK_RSHIFT + [0xa2, UiohookKey.Ctrl], // VK_LCONTROL + [0xa3, UiohookKey.CtrlRight], // VK_RCONTROL + [0xa4, UiohookKey.Alt], // VK_LMENU + [0xa5, UiohookKey.AltRight], // VK_RMENU (Right Alt) + [0x5b, UiohookKey.Meta], // VK_LWIN + [0x5c, UiohookKey.MetaRight], // VK_RWIN + + // 기능 키 + [0x70, UiohookKey.F1], + [0x71, UiohookKey.F2], + [0x72, UiohookKey.F3], + [0x73, UiohookKey.F4], + [0x74, UiohookKey.F5], + [0x75, UiohookKey.F6], + [0x76, UiohookKey.F7], + [0x77, UiohookKey.F8], + [0x78, UiohookKey.F9], + [0x79, UiohookKey.F10], + [0x7a, UiohookKey.F11], + [0x7b, UiohookKey.F12], + + // 일반 키 + [0x20, UiohookKey.Space], + [0x0d, UiohookKey.Enter], + [0x1b, UiohookKey.Escape], + [0x08, UiohookKey.Backspace], + [0x09, UiohookKey.Tab], + [0x2d, UiohookKey.Insert], + [0x2e, UiohookKey.Delete], + [0x24, UiohookKey.Home], + [0x23, UiohookKey.End], + [0x21, UiohookKey.PageUp], + [0x22, UiohookKey.PageDown], + [0x25, UiohookKey.ArrowLeft], + [0x26, UiohookKey.ArrowUp], + [0x27, UiohookKey.ArrowRight], + [0x28, UiohookKey.ArrowDown], + + // 알파벳 (VK_A=0x41 ~ VK_Z=0x5A) + [0x41, UiohookKey.A], + [0x42, UiohookKey.B], + [0x43, UiohookKey.C], + [0x44, UiohookKey.D], + [0x45, UiohookKey.E], + [0x46, UiohookKey.F], + [0x47, UiohookKey.G], + [0x48, UiohookKey.H], + [0x49, UiohookKey.I], + [0x4a, UiohookKey.J], + [0x4b, UiohookKey.K], + [0x4c, UiohookKey.L], + [0x4d, UiohookKey.M], + [0x4e, UiohookKey.N], + [0x4f, UiohookKey.O], + [0x50, UiohookKey.P], + [0x51, UiohookKey.Q], + [0x52, UiohookKey.R], + [0x53, UiohookKey.S], + [0x54, UiohookKey.T], + [0x55, UiohookKey.U], + [0x56, UiohookKey.V], + [0x57, UiohookKey.W], + [0x58, UiohookKey.X], + [0x59, UiohookKey.Y], + [0x5a, UiohookKey.Z], + + // 숫자 (VK_0=0x30 ~ VK_9=0x39) + [0x30, UiohookKey['0']], + [0x31, UiohookKey['1']], + [0x32, UiohookKey['2']], + [0x33, UiohookKey['3']], + [0x34, UiohookKey['4']], + [0x35, UiohookKey['5']], + [0x36, UiohookKey['6']], + [0x37, UiohookKey['7']], + [0x38, UiohookKey['8']], + [0x39, UiohookKey['9']] +]) + +/** + * HotkeyBinding(Windows VK 코드)을 HotkeyConfig(uiohook 키코드)로 변환한다. + */ +function bindingToConfig( + id: string, + binding: HotkeyBinding, + holdMode: boolean, + doublePressEnabled: boolean +): HotkeyConfig { + const uiohookKeyCode = VK_TO_UIOHOOK.get(binding.keyCode) + + if (uiohookKeyCode === undefined) { + logger.warn( + `Unknown VK code 0x${binding.keyCode.toString(16)} for hotkey "${id}", ` + + `using raw value ${binding.keyCode}` + ) + } + + const modifiers: HotkeyModifier[] = [] + if (binding.ctrl) modifiers.push('ctrl') + if (binding.alt) modifiers.push('alt') + if (binding.shift) modifiers.push('shift') + if (binding.meta) modifiers.push('meta') + + return { + id, + keyCode: uiohookKeyCode ?? binding.keyCode, + modifiers, + holdMode, + doublePressEnabled, + enabled: true + } +} + +// ============================================================ +// HotkeyService 클래스 +// ============================================================ + +class HotkeyService extends EventEmitter { + private _isRunning = false + private _registeredHotkeys: Map = new Map() + + /** 더블프레스 감지용: 마지막 press 시각 */ + private _lastPressTime: Map = new Map() + + /** hold duration 계산용: press 시작 시각 */ + private _pressStartTime: Map = new Map() + + /** 키 반복(auto-repeat) 방지: 현재 눌려있는 키 */ + private _isKeyDown: Map = new Map() + + /** uiohook 이벤트 핸들러 (바인딩 해제용) */ + private _onKeyDown: ((e: UiohookKeyboardEvent) => void) | null = null + private _onKeyUp: ((e: UiohookKeyboardEvent) => void) | null = null + + get isRunning(): boolean { + return this._isRunning + } + + get registeredHotkeys(): ReadonlyMap { + return this._registeredHotkeys + } + + /** + * uiohook 글로벌 키보드 후킹을 시작한다. + * 이미 실행 중이면 무시. + */ + start(): void { + if (this._isRunning) { + logger.warn('HotkeyService already running') + return + } + + try { + this._onKeyDown = (e: UiohookKeyboardEvent) => this._handleKeyDown(e) + this._onKeyUp = (e: UiohookKeyboardEvent) => this._handleKeyUp(e) + + uIOhook.on('keydown', this._onKeyDown) + uIOhook.on('keyup', this._onKeyUp) + uIOhook.start() + + this._isRunning = true + logger.info('uiohook started, global keyboard hook active') + } catch (error) { + const d3roError = new D3ROError( + ErrorCode.HotkeyHookInitFailed, + `Failed to start uiohook: ${error instanceof Error ? error.message : String(error)}` + ) + this.emit('error', { error: d3roError }) + logger.error(d3roError.message) + } + } + + /** + * uiohook 글로벌 키보드 후킹을 중지한다. + */ + stop(): void { + if (!this._isRunning) { + return + } + + try { + uIOhook.stop() + + if (this._onKeyDown) { + uIOhook.removeListener('keydown', this._onKeyDown) + this._onKeyDown = null + } + if (this._onKeyUp) { + uIOhook.removeListener('keyup', this._onKeyUp) + this._onKeyUp = null + } + + this._isRunning = false + this._isKeyDown.clear() + this._pressStartTime.clear() + logger.info('uiohook stopped') + } catch (error) { + logger.error( + `Failed to stop uiohook: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + /** + * 핫키를 등록한다. 동일 ID가 이미 등록되어 있으면 갱신한다. + */ + registerHotkey(config: HotkeyConfig): void { + if (!config.enabled) { + logger.debug(`Hotkey "${config.id}" is disabled, skipping registration`) + return + } + + this._registeredHotkeys.set(config.id, config) + logger.info( + `Hotkey registered: "${config.id}" ` + + `(keyCode=${config.keyCode}, modifiers=[${config.modifiers.join(',')}], ` + + `holdMode=${config.holdMode}, doublePress=${config.doublePressEnabled})` + ) + } + + /** + * 핫키 등록을 해제한다. + */ + unregisterHotkey(id: string): void { + if (this._registeredHotkeys.delete(id)) { + this._lastPressTime.delete(id) + this._pressStartTime.delete(id) + this._isKeyDown.delete(id) + logger.info(`Hotkey unregistered: "${id}"`) + } + } + + /** + * ConfigService에서 핫키 설정을 로드하여 등록한다. + */ + loadFromConfig(): void { + const hotkeyEnabled = configGet('hotkeyEnabled') + if (!hotkeyEnabled) { + logger.info('Hotkeys disabled in config') + return + } + + const dictationBinding = configGet('dictationShortcut') + const handsFreeBinding = configGet('handsFreeShortcut') + const commandBinding = configGet('commandShortcut') + + // 기존 핫키 초기화 + this._registeredHotkeys.clear() + + // Dictation: hold-to-talk, 더블프레스 비활성 + this.registerHotkey( + bindingToConfig('voice-dictation', dictationBinding, true, false) + ) + + // Hands-free: toggle, 더블프레스 활성 + this.registerHotkey( + bindingToConfig('voice-handsfree', handsFreeBinding, false, true) + ) + + // Command: toggle, 더블프레스 비활성 + this.registerHotkey( + bindingToConfig('voice-command', commandBinding, false, false) + ) + + logger.info( + `Loaded ${this._registeredHotkeys.size} hotkeys from config` + ) + } + + /** + * 서비스 리소스를 정리한다. + */ + dispose(): void { + this.stop() + this._registeredHotkeys.clear() + this._lastPressTime.clear() + this._pressStartTime.clear() + this._isKeyDown.clear() + this.removeAllListeners() + logger.info('HotkeyService disposed') + } + + // ============================================================ + // 내부: 키 이벤트 처리 + // ============================================================ + + /** + * 키 다운 이벤트를 처리한다. + * 등록된 핫키 중 매칭되는 것을 찾아 적절한 이벤트를 발행한다. + */ + private _handleKeyDown(e: UiohookKeyboardEvent): void { + const matched = this._findMatchingHotkey(e) + if (!matched) return + + const { id } = matched + + // 키 반복(auto-repeat) 무시: 이미 눌려있으면 건너뜀 + if (this._isKeyDown.get(id)) { + return + } + this._isKeyDown.set(id, true) + + const now = Date.now() + + // press 시작 시각 기록 (hold duration 계산용) + this._pressStartTime.set(id, now) + + // 더블프레스 감지 + if (matched.doublePressEnabled) { + const lastPress = this._lastPressTime.get(id) + + if (lastPress !== undefined && now - lastPress < TIMING.DOUBLE_PRESS_DURATION) { + // 300ms 이내 연속 두 번 press = 더블프레스 + this._lastPressTime.delete(id) + + logger.debug(`Double-press detected: "${id}" (interval=${now - lastPress}ms)`) + this.emit('double-press', { + config: matched, + intervalMs: now - lastPress, + timestamp: now + }) + return + } + + this._lastPressTime.set(id, now) + } + + logger.debug(`Hotkey pressed: "${id}"`) + this.emit('hotkey-pressed', { + config: matched, + timestamp: now + }) + } + + /** + * 키 업 이벤트를 처리한다. + */ + private _handleKeyUp(e: UiohookKeyboardEvent): void { + const matched = this._findMatchingHotkey(e) + if (!matched) return + + const { id } = matched + + // 눌려있지 않은 키의 release는 무시 + if (!this._isKeyDown.get(id)) { + return + } + this._isKeyDown.set(id, false) + + const now = Date.now() + const pressStart = this._pressStartTime.get(id) + const durationMs = pressStart !== undefined ? now - pressStart : 0 + this._pressStartTime.delete(id) + + logger.debug(`Hotkey released: "${id}" (duration=${durationMs}ms)`) + this.emit('hotkey-released', { + config: matched, + durationMs, + timestamp: now + }) + } + + /** + * uiohook 키 이벤트와 등록된 핫키를 매칭한다. + * 키코드와 수정자 키가 모두 일치해야 매칭 성공. + */ + private _findMatchingHotkey(e: UiohookKeyboardEvent): HotkeyConfig | null { + for (const config of this._registeredHotkeys.values()) { + if (!config.enabled) continue + + // 키코드 일치 확인 + if (e.keycode !== config.keyCode) continue + + // 수정자 키 일치 확인 + // 핫키에 지정된 수정자가 모두 눌려 있어야 하고, + // 지정되지 않은 수정자는 눌려 있으면 안 된다. + // + // 단, 핫키의 주 키 자체가 수정자 키인 경우(예: Right Alt)에는 + // 해당 수정자의 altKey 등이 true로 올 수 있으므로, 주 키가 수정자인 경우 + // 해당 수정자 검사를 건너뛴다. + const isKeyModifier = this._isModifierKeyCode(config.keyCode) + + const wantsCtrl = config.modifiers.includes('ctrl') + const wantsAlt = config.modifiers.includes('alt') + const wantsShift = config.modifiers.includes('shift') + const wantsMeta = config.modifiers.includes('meta') + + const ctrlMatch = isKeyModifier && this._isCtrlKeyCode(config.keyCode) + ? true + : e.ctrlKey === wantsCtrl + const altMatch = isKeyModifier && this._isAltKeyCode(config.keyCode) + ? true + : e.altKey === wantsAlt + const shiftMatch = isKeyModifier && this._isShiftKeyCode(config.keyCode) + ? true + : e.shiftKey === wantsShift + const metaMatch = isKeyModifier && this._isMetaKeyCode(config.keyCode) + ? true + : e.metaKey === wantsMeta + + if (ctrlMatch && altMatch && shiftMatch && metaMatch) { + return config + } + } + + return null + } + + /** 주어진 uiohook 키코드가 수정자 키(Ctrl/Alt/Shift/Meta)인지 확인 */ + private _isModifierKeyCode(keyCode: number): boolean { + return ( + this._isCtrlKeyCode(keyCode) || + this._isAltKeyCode(keyCode) || + this._isShiftKeyCode(keyCode) || + this._isMetaKeyCode(keyCode) + ) + } + + private _isCtrlKeyCode(keyCode: number): boolean { + return keyCode === UiohookKey.Ctrl || keyCode === UiohookKey.CtrlRight + } + + private _isAltKeyCode(keyCode: number): boolean { + return keyCode === UiohookKey.Alt || keyCode === UiohookKey.AltRight + } + + private _isShiftKeyCode(keyCode: number): boolean { + return keyCode === UiohookKey.Shift || keyCode === UiohookKey.ShiftRight + } + + private _isMetaKeyCode(keyCode: number): boolean { + return keyCode === UiohookKey.Meta || keyCode === UiohookKey.MetaRight + } + + // ============================================================ + // EventEmitter 타입 오버라이드 + // ============================================================ + + override on( + event: K, + listener: HotkeyServiceEvents[K] + ): this { + return super.on(event, listener) + } + + override off( + event: K, + listener: HotkeyServiceEvents[K] + ): this { + return super.off(event, listener) + } + + override emit( + event: K, + ...args: Parameters + ): boolean { + return super.emit(event, ...args) + } +} + +// ============================================================ +// 싱글톤 +// ============================================================ + +let instance: HotkeyService | null = null + +export function getHotkeyService(): HotkeyService { + if (!instance) { + instance = new HotkeyService() + } + return instance +} diff --git a/src/main/services/LocalSTTService.ts b/src/main/services/LocalSTTService.ts new file mode 100644 index 0000000..6f8d3e3 --- /dev/null +++ b/src/main/services/LocalSTTService.ts @@ -0,0 +1,729 @@ +// src/main/services/LocalSTTService.ts +// faster-whisper sidecar를 관리하고 오디오 버퍼를 텍스트로 변환한다. +// 싱글톤 + EventEmitter 패턴. Speakly VoiceRecognitionService의 +// 상태 머신 및 이중 조건 플러시 패턴 적용. + +import { EventEmitter } from 'events' +import { type ChildProcess, spawn } from 'child_process' +import path from 'path' +import { app } from 'electron' +import { getLogger } from './LoggerService' +import { configGet } from './ConfigService' +import { D3ROError, ErrorCode } from '@shared/errors' +import type { STTModel, STTStatus, STTEngineState } from '@shared/types' + +// ── 내부 타입 정의 ──────────────────────────────────────── + +/** STT 엔진 상태 머신 */ +const enum STTState { + Uninitialized = 'uninitialized', + Loading = 'loading', + Ready = 'ready', + Transcribing = 'transcribing', + Error = 'error', +} + +/** 전사 결과 세그먼트 */ +export interface TranscriptionSegment { + readonly text: string + readonly start: number + readonly end: number + readonly confidence: number +} + +/** 전사 결과 */ +export interface TranscriptionResult { + readonly text: string + readonly segments: TranscriptionSegment[] + readonly language: string + readonly duration: number + readonly processingTime: number +} + +/** 전사 옵션 */ +export interface TranscribeOptions { + language?: string + initialPrompt?: string + vadFilter?: boolean +} + +/** sidecar /health 응답 */ +interface HealthResponse { + status: string + model: string | null + gpu: boolean +} + +/** sidecar /load 응답 */ +interface LoadResponse { + status: string + model_id: string + load_time_ms: number +} + +/** sidecar /transcribe 응답 */ +interface TranscribeResponse { + text: string + segments: Array<{ + text: string + start: number + end: number + avg_logprob: number + }> + language: string + duration: number + processing_time: number +} + +/** 이벤트 페이로드 */ +export interface LocalSTTEvents { + 'transcription-delta': { text: string; isFinal: boolean } + 'transcription-complete': { result: TranscriptionResult } + 'model-loaded': { model: STTModel; loadTimeMs: number } + 'error': { error: D3ROError } +} + +// ── 상수 ────────────────────────────────────────────────── + +const SIDECAR_PORT = 18765 +const HEALTH_CHECK_INTERVAL_MS = 1000 +const HEALTH_CHECK_TIMEOUT_MS = 30000 +const MAX_RESTART_COUNT = 3 +const SIDECAR_REQUEST_TIMEOUT_MS = 120000 + +/** 알려진 Whisper 모델 카탈로그 */ +const MODEL_CATALOG: STTModel[] = [ + { + id: 'tiny', + name: 'Tiny', + sizeBytes: 75_000_000, + downloaded: false, + languages: ['auto', 'ko', 'en', 'ja', 'zh'], + accuracy: 1, + speed: 5, + }, + { + id: 'base', + name: 'Base', + sizeBytes: 141_000_000, + downloaded: false, + languages: ['auto', 'ko', 'en', 'ja', 'zh'], + accuracy: 2, + speed: 4, + }, + { + id: 'small', + name: 'Small', + sizeBytes: 466_000_000, + downloaded: false, + languages: ['auto', 'ko', 'en', 'ja', 'zh'], + accuracy: 3, + speed: 3, + }, + { + id: 'medium', + name: 'Medium', + sizeBytes: 1_500_000_000, + downloaded: false, + languages: ['auto', 'ko', 'en', 'ja', 'zh'], + accuracy: 4, + speed: 2, + }, + { + id: 'large-v3', + name: 'Large V3', + sizeBytes: 3_100_000_000, + downloaded: false, + languages: ['auto', 'ko', 'en', 'ja', 'zh'], + accuracy: 5, + speed: 1, + }, +] + +// ── 서비스 구현 ─────────────────────────────────────────── + +const logger = getLogger('LocalSTTService') + +class LocalSTTService extends EventEmitter { + private _state: STTState = STTState.Uninitialized + private _sidecarProcess: ChildProcess | null = null + private _port: number = SIDECAR_PORT + private _currentModelId: string | null = null + private _restartCount: number = 0 + private _disposed: boolean = false + private _gpuAccelerated: boolean = false + + // ── 이중 조건 플러시 (Speakly 패턴) ── + private _modelReady: boolean = false + private _audioBuffer: Buffer[] = [] + private _pendingResolve: ((result: TranscriptionResult) => void) | null = null + private _pendingReject: ((error: D3ROError) => void) | null = null + // errorEmitted 플래그로 이벤트 중복 방지 + private _errorEmitted: boolean = false + + // ── 상태 접근자 ── + + get state(): STTState { + return this._state + } + + get currentModelId(): string | null { + return this._currentModelId + } + + // ── 공개 메서드 ── + + /** + * Whisper sidecar 프로세스 시작 + 모델 로딩. + * 이미 로딩된 모델과 같으면 무시. + */ + async initialize(modelId?: string): Promise { + const targetModel = modelId ?? configGet('sttModelId') + + if (this._disposed) { + throw new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + 'LocalSTTService가 이미 dispose되었습니다', + ) + } + + // 이미 같은 모델이 로딩된 상태면 무시 + if ( + this._state === STTState.Ready && + this._currentModelId === targetModel + ) { + logger.debug(`모델 ${targetModel}이 이미 로딩되어 있습니다`) + return + } + + this._setState(STTState.Loading) + this._errorEmitted = false + + try { + // sidecar가 아직 실행 중이 아니면 시작 + if (!this._sidecarProcess || this._sidecarProcess.exitCode !== null) { + await this._spawnSidecar() + await this._waitForHealth() + } + + // 모델 로딩 + await this._loadModel(targetModel) + this._currentModelId = targetModel + this._modelReady = true + this._setState(STTState.Ready) + + // 이중 조건 플러시 시도 + this._tryFlushAll() + + logger.info(`STT 초기화 완료: 모델=${targetModel}`) + } catch (err) { + this._setState(STTState.Error) + const d3roErr = + err instanceof D3ROError + ? err + : new D3ROError( + ErrorCode.STTModelLoadFailed, + `STT 초기화 실패: ${err instanceof Error ? err.message : String(err)}`, + ) + this._emitError(d3roErr) + throw d3roErr + } + } + + /** + * 오디오 버퍼를 전사. + * PCM16 16kHz mono 포맷이어야 한다. + * 이중 조건 플러시: 모델 로딩과 오디오 버퍼링이 모두 완료되면 실행. + */ + async transcribe( + audioBuffer: Buffer, + options?: TranscribeOptions, + ): Promise { + if (this._disposed) { + throw new D3ROError( + ErrorCode.STTTranscriptionFailed, + 'LocalSTTService가 이미 dispose되었습니다', + ) + } + + if (audioBuffer.length === 0) { + throw new D3ROError(ErrorCode.STTNoAudioData, '오디오 데이터가 비어있습니다') + } + + // 모델이 아직 준비되지 않았으면 버퍼에 적재하고 대기 + if (!this._modelReady) { + logger.debug('모델 로딩 중, 오디오 버퍼에 적재') + this._audioBuffer.push(audioBuffer) + + return new Promise((resolve, reject) => { + this._pendingResolve = resolve + this._pendingReject = reject + // 이중 조건 플러시 시도 (모델이 이미 준비되었을 수 있음) + this._tryFlushAll() + }) + } + + // 모델 준비 완료 상태: 직접 전사 + return this._sendToSidecar(audioBuffer, options) + } + + /** + * 다운로드된 모델 목록 조회. + * 실제 다운로드 여부는 sidecar에 위임 (faster-whisper가 자동 다운로드). + */ + getModels(): STTModel[] { + return MODEL_CATALOG.map((m) => ({ + ...m, + // 현재 로딩된 모델은 downloaded=true로 표시 + downloaded: m.id === this._currentModelId ? true : m.downloaded, + })) + } + + /** 현재 상태 조회 */ + getStatus(): STTStatus { + const stateMap: Record = { + [STTState.Uninitialized]: 'not-installed' as STTEngineState, + [STTState.Loading]: 'loading' as STTEngineState, + [STTState.Ready]: 'ready' as STTEngineState, + [STTState.Transcribing]: 'processing' as STTEngineState, + [STTState.Error]: 'error' as STTEngineState, + } + + return { + engineState: stateMap[this._state], + activeModel: this._currentModelId, + engineVersion: null, + gpuAccelerated: this._gpuAccelerated, + } + } + + /** sidecar 프로세스 종료 및 리소스 정리 */ + async dispose(): Promise { + if (this._disposed) return + this._disposed = true + + logger.info('LocalSTTService dispose 시작') + + // pending promise를 reject + if (this._pendingReject) { + this._pendingReject( + new D3ROError(ErrorCode.STTTranscriptionCancelled, '서비스 종료로 전사 취소'), + ) + this._pendingResolve = null + this._pendingReject = null + } + + this._audioBuffer = [] + this._modelReady = false + + await this._shutdownSidecar() + this._setState(STTState.Uninitialized) + + logger.info('LocalSTTService dispose 완료') + } + + // ── 이중 조건 플러시 (Speakly 핵심 패턴) ── + + /** + * 모델 로딩과 오디오 버퍼링이 모두 완료되면 실행. + * 설정 메시지(모델) 먼저, 오디오 데이터 후. + */ + private _tryFlushAll(): void { + if (!this._modelReady) return + if (this._audioBuffer.length === 0) return + if (!this._pendingResolve) return + + const merged = Buffer.concat(this._audioBuffer) + this._audioBuffer = [] + + const resolve = this._pendingResolve + const reject = this._pendingReject + this._pendingResolve = null + this._pendingReject = null + + this._sendToSidecar(merged) + .then(resolve) + .catch((err: unknown) => { + if (reject) { + reject( + err instanceof D3ROError + ? err + : new D3ROError( + ErrorCode.STTTranscriptionFailed, + `전사 실패: ${err instanceof Error ? err.message : String(err)}`, + ), + ) + } + }) + } + + // ── Sidecar 관리 ── + + private _getSidecarPath(): string { + const basePath = app.isPackaged ? process.resourcesPath : app.getAppPath() + return path.join(basePath, 'sidecar', 'main.py') + } + + private async _spawnSidecar(): Promise { + const sidecarPath = this._getSidecarPath() + logger.info(`Sidecar 시작: python ${sidecarPath} --port ${this._port}`) + + return new Promise((resolve, reject) => { + const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' + + try { + this._sidecarProcess = spawn( + pythonCmd, + [sidecarPath, '--port', String(this._port)], + { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env }, + }, + ) + } catch (err) { + const d3roErr = new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `Sidecar 프로세스 생성 실패: ${err instanceof Error ? err.message : String(err)}`, + ) + reject(d3roErr) + return + } + + const sidecarLogger = getLogger('sidecar') + + this._sidecarProcess.stdout?.on('data', (data: Buffer) => { + const text = data.toString().trim() + if (text) { + sidecarLogger.info(text) + } + }) + + this._sidecarProcess.stderr?.on('data', (data: Buffer) => { + const text = data.toString().trim() + if (text) { + sidecarLogger.warn(text) + } + }) + + this._sidecarProcess.on('error', (err: Error) => { + logger.error(`Sidecar 프로세스 에러: ${err.message}`) + reject( + new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `Sidecar 프로세스 에러: ${err.message}`, + ), + ) + }) + + this._sidecarProcess.on('exit', (code: number | null, signal: string | null) => { + logger.warn(`Sidecar 프로세스 종료: code=${code}, signal=${signal}`) + this._sidecarProcess = null + this._modelReady = false + + if (!this._disposed) { + this._handleSidecarCrash() + } + }) + + // spawn 자체는 비동기적이므로 즉시 resolve + // 실제 준비는 _waitForHealth에서 확인 + resolve() + }) + } + + private async _waitForHealth(): Promise { + const startTime = Date.now() + + while (Date.now() - startTime < HEALTH_CHECK_TIMEOUT_MS) { + try { + const response = await fetch(`http://localhost:${this._port}/health`, { + signal: AbortSignal.timeout(2000), + }) + + if (response.ok) { + const data = (await response.json()) as HealthResponse + this._gpuAccelerated = data.gpu + logger.info( + `Sidecar 헬스체크 성공: status=${data.status}, gpu=${data.gpu}`, + ) + return + } + } catch { + // 아직 준비 안 됨, 재시도 + } + + await this._sleep(HEALTH_CHECK_INTERVAL_MS) + } + + throw new D3ROError( + ErrorCode.STTSidecarCommunicationFailed, + `Sidecar 헬스체크 타임아웃 (${HEALTH_CHECK_TIMEOUT_MS}ms)`, + ) + } + + private async _loadModel(modelId: string): Promise { + logger.info(`모델 로딩 시작: ${modelId}`) + const startTime = Date.now() + + const response = await fetch(`http://localhost:${this._port}/load`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model_id: modelId }), + signal: AbortSignal.timeout(SIDECAR_REQUEST_TIMEOUT_MS), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new D3ROError( + ErrorCode.STTModelLoadFailed, + `모델 로딩 실패 (HTTP ${response.status}): ${errorText}`, + ) + } + + const data = (await response.json()) as LoadResponse + const loadTimeMs = Date.now() - startTime + + const model = MODEL_CATALOG.find((m) => m.id === modelId) + if (model) { + this.emit('model-loaded', { + model: { ...model, downloaded: true }, + loadTimeMs, + }) + } + + logger.info( + `모델 로딩 완료: ${data.model_id}, ${loadTimeMs}ms`, + ) + } + + // ── HTTP 전사 요청 ── + + private async _sendToSidecar( + audioBuffer: Buffer, + options?: TranscribeOptions, + ): Promise { + if (!this._sidecarProcess || this._sidecarProcess.exitCode !== null) { + throw new D3ROError( + ErrorCode.STTSidecarCommunicationFailed, + 'Sidecar 프로세스가 실행 중이 아닙니다', + ) + } + + this._setState(STTState.Transcribing) + const startTime = Date.now() + + try { + const language = options?.language ?? configGet('sttLanguage') + const vadFilter = options?.vadFilter ?? true + const initialPrompt = options?.initialPrompt ?? '' + + // Node 18+ 내장 fetch + FormData + Blob으로 multipart 전송 + const formData = new FormData() + // Buffer → ArrayBuffer 복사 후 Blob 생성 (Node/Electron 타입 호환) + const arrayBuf = audioBuffer.buffer.slice( + audioBuffer.byteOffset, + audioBuffer.byteOffset + audioBuffer.byteLength, + ) as ArrayBuffer + formData.append( + 'audio', + new Blob([arrayBuf], { type: 'application/octet-stream' }), + 'audio.pcm', + ) + formData.append('language', language) + formData.append('vad_filter', String(vadFilter)) + if (initialPrompt) { + formData.append('initial_prompt', initialPrompt) + } + + const response = await fetch( + `http://localhost:${this._port}/transcribe`, + { + method: 'POST', + body: formData, + signal: AbortSignal.timeout(SIDECAR_REQUEST_TIMEOUT_MS), + }, + ) + + if (!response.ok) { + const errorText = await response.text() + throw new D3ROError( + ErrorCode.STTTranscriptionFailed, + `전사 실패 (HTTP ${response.status}): ${errorText}`, + ) + } + + const data = (await response.json()) as TranscribeResponse + const processingTime = Date.now() - startTime + + const result: TranscriptionResult = { + text: data.text, + segments: data.segments.map((seg) => ({ + text: seg.text, + start: seg.start, + end: seg.end, + confidence: Math.exp(seg.avg_logprob), + })), + language: data.language, + duration: data.duration, + processingTime, + } + + // 중간 결과 이벤트 (isFinal=true) + this.emit('transcription-delta', { text: result.text, isFinal: true }) + this.emit('transcription-complete', { result }) + + this._setState(STTState.Ready) + + logger.info( + `전사 완료: "${result.text.substring(0, 50)}..." (${processingTime}ms, lang=${result.language})`, + ) + + return result + } catch (err) { + this._setState(STTState.Ready) // 에러 후에도 Ready 복귀 (sidecar가 살아있으면) + + if (err instanceof D3ROError) { + throw err + } + + const message = err instanceof Error ? err.message : String(err) + + // 타임아웃 구분 + if (message.includes('abort') || message.includes('timeout')) { + throw new D3ROError( + ErrorCode.STTTranscriptionTimeout, + `전사 타임아웃: ${message}`, + ) + } + + throw new D3ROError( + ErrorCode.STTTranscriptionFailed, + `전사 실패: ${message}`, + ) + } + } + + // ── Sidecar Crash 처리 ── + + private _handleSidecarCrash(): void { + if (this._disposed) return + + this._restartCount++ + logger.warn(`Sidecar crash 감지, 재시작 시도 ${this._restartCount}/${MAX_RESTART_COUNT}`) + + if (this._restartCount > MAX_RESTART_COUNT) { + const err = new D3ROError( + ErrorCode.STTSidecarCrashed, + `Sidecar가 ${MAX_RESTART_COUNT}회 crash 후 재시작 포기`, + ) + this._setState(STTState.Error) + this._emitError(err) + + // pending promise reject + if (this._pendingReject) { + this._pendingReject(err) + this._pendingResolve = null + this._pendingReject = null + } + return + } + + // 비동기 재시작 + const modelToReload = this._currentModelId + this._currentModelId = null + this._modelReady = false + + // setTimeout으로 이벤트 루프에 양보 + setTimeout(() => { + if (this._disposed) return + this.initialize(modelToReload ?? undefined).catch((err: unknown) => { + logger.error( + `Sidecar 재시작 실패: ${err instanceof Error ? err.message : String(err)}`, + ) + }) + }, 1000 * this._restartCount) // 점진적 백오프 + } + + private async _shutdownSidecar(): Promise { + if (!this._sidecarProcess || this._sidecarProcess.exitCode !== null) { + this._sidecarProcess = null + return + } + + logger.info('Sidecar 종료 요청') + + try { + // POST /shutdown 요청 + await fetch(`http://localhost:${this._port}/shutdown`, { + method: 'POST', + signal: AbortSignal.timeout(3000), + }) + } catch { + // 이미 종료되었거나 통신 불가 — 무시 + } + + // 프로세스가 아직 살아있으면 강제 종료 + if (this._sidecarProcess && this._sidecarProcess.exitCode === null) { + logger.warn('Sidecar graceful shutdown 실패, SIGKILL 전송') + this._sidecarProcess.kill('SIGKILL') + } + + this._sidecarProcess = null + } + + // ── 내부 유틸 ── + + private _setState(newState: STTState): void { + if (this._state === newState) return + const prev = this._state + this._state = newState + logger.debug(`STTState: ${prev} -> ${newState}`) + } + + private _emitError(error: D3ROError): void { + if (this._errorEmitted) return + this._errorEmitted = true + this.emit('error', { error }) + logger.error(`STT 에러: [${error.code}] ${error.message}`) + } + + private _sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + // ── 타입 안전한 이벤트 메서드 오버라이드 ── + + override emit( + event: K, + payload: LocalSTTEvents[K], + ): boolean { + return super.emit(event, payload) + } + + override on( + event: K, + listener: (payload: LocalSTTEvents[K]) => void, + ): this { + return super.on(event, listener) + } + + override off( + event: K, + listener: (payload: LocalSTTEvents[K]) => void, + ): this { + return super.off(event, listener) + } +} + +// ── 싱글톤 ── + +let instance: LocalSTTService | null = null + +export function getLocalSTTService(): LocalSTTService { + if (!instance) { + instance = new LocalSTTService() + } + return instance +} + +export { LocalSTTService, STTState } diff --git a/src/main/services/LoggerService.ts b/src/main/services/LoggerService.ts new file mode 100644 index 0000000..aefed5c --- /dev/null +++ b/src/main/services/LoggerService.ts @@ -0,0 +1,32 @@ +// src/main/services/LoggerService.ts +// electron-log 래퍼. 카테고리별 로깅을 지원한다. + +import log from 'electron-log' + +export interface CategoryLogger { + info(message: string, ...args: unknown[]): void + warn(message: string, ...args: unknown[]): void + error(message: string, ...args: unknown[]): void + debug(message: string, ...args: unknown[]): void +} + +let initialized = false + +export function initLoggerService(): void { + if (initialized) return + initialized = true + + log.transports.file.maxSize = 5 * 1024 * 1024 // 5MB + log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}' + log.transports.console.format = '[{h}:{i}:{s}.{ms}] [{level}] {text}' +} + +export function getLogger(category: string): CategoryLogger { + const prefix = `[${category}]` + return { + info: (message: string, ...args: unknown[]) => log.info(`${prefix} ${message}`, ...args), + warn: (message: string, ...args: unknown[]) => log.warn(`${prefix} ${message}`, ...args), + error: (message: string, ...args: unknown[]) => log.error(`${prefix} ${message}`, ...args), + debug: (message: string, ...args: unknown[]) => log.debug(`${prefix} ${message}`, ...args) + } +} diff --git a/src/main/services/VoiceModeService.ts b/src/main/services/VoiceModeService.ts new file mode 100644 index 0000000..99a68ff --- /dev/null +++ b/src/main/services/VoiceModeService.ts @@ -0,0 +1,625 @@ +// src/main/services/VoiceModeService.ts +// 전체 음성 파이프라인 오케스트레이터. +// 설계서 01의 IVoiceModeService 구현. Speakly VoiceModeService의 +// 상태 머신, 이중 조건 플러시, Action Queue 패턴 적용. + +import { EventEmitter } from 'events' +import { randomUUID } from 'crypto' +import { getLogger } from './LoggerService' +import { getAudioCaptureService } from './AudioCaptureService' +import { getLocalSTTService } from './LocalSTTService' +import type { TranscriptionResult } from './LocalSTTService' +import { getHotkeyService } from './HotkeyService' +import type { HotkeyConfig } from './HotkeyService' +import { configGet } from './ConfigService' +import { D3ROError, ErrorCode } from '@shared/errors' +import { TIMING } from '@shared/constants' +import { RecognitionState, AudioState } from '@shared/types' +import type { VoiceMode, VoiceState } from '@shared/types' + +const logger = getLogger('VoiceModeService') + +// ============================================================ +// 내부 타입 +// ============================================================ + +interface VoiceSession { + id: string + mode: VoiceMode + startedAt: number + recognitionState: RecognitionState + audioState: AudioState + audioBufferDurationMs: number + transcription: string + processedText: string | null + accidentalPress: boolean +} + +interface VoiceAction { + type: 'press' | 'release' | 'escape' + timestamp: number + mode: VoiceMode + hotkeyId: string +} + +interface VoiceModeEvents { + 'session-started': (payload: { session: VoiceSession }) => void + 'recognition-state-changed': (payload: { + previous: RecognitionState + current: RecognitionState + }) => void + 'audio-state-changed': (payload: { + previous: AudioState + current: AudioState + }) => void + 'transcription-update': (payload: { text: string; isFinal: boolean }) => void + 'session-completed': (payload: { session: VoiceSession; finalText: string }) => void + 'session-cancelled': (payload: { + session: VoiceSession + reason: 'user' | 'timeout' | 'too-short' + }) => void + 'audio-level': (payload: { level: number }) => void + error: (payload: { error: D3ROError; session: VoiceSession | null }) => void +} + +// ============================================================ +// 터미널 상태 헬퍼 +// ============================================================ + +const TERMINAL_STATES = new Set([ + RecognitionState.COMPLETED, + RecognitionState.CANCELLED, + RecognitionState.ERROR, + RecognitionState.DESTROYED +]) + +// ============================================================ +// VoiceModeService +// ============================================================ + +class VoiceModeService extends EventEmitter { + private _session: VoiceSession | null = null + private _recognitionState = RecognitionState.IDLE + private _audioState = AudioState.IDLE + + // 이중 조건 플러시 + private _sttReady = false + private _audioStarted = false + private _audioBuffer: Buffer[] = [] + private _audioBufferBytes = 0 + + // 에러 가드 + private _errorEmitted = false + + // Action Queue (이벤트 직렬화) + private _actionQueue: VoiceAction[] = [] + private _isProcessingQueue = false + + // 리스너 해제용 참조 + private _audioDataHandler: ((payload: { buffer: Buffer }) => void) | null = null + private _audioLevelHandler: ((payload: { level: number }) => void) | null = null + private _hotkeyPressHandler: ((payload: { config: HotkeyConfig; timestamp: number }) => void) | null = null + private _hotkeyReleaseHandler: ((payload: { config: HotkeyConfig; durationMs: number; timestamp: number }) => void) | null = null + private _doublePressHandler: ((payload: { config: HotkeyConfig }) => void) | null = null + + private _disposed = false + + get currentSession(): VoiceSession | null { + return this._session + } + + get isActive(): boolean { + return this._session !== null && !this._isInTerminalState() + } + + getState(): VoiceState { + return { + recognitionState: this._recognitionState, + audioState: this._audioState, + mode: configGet('defaultLLMAction') === 'translate' ? 'hands-free' : 'dictation', + sessionId: this._session?.id ?? null, + recordingStartedAt: this._session?.startedAt ?? null + } + } + + // ── 초기화 ────────────────────────────────────────────── + + /** + * HotkeyService 이벤트를 구독하여 핫키 → 세션 제어를 연결한다. + * bootstrap에서 호출한다. + */ + connectHotkey(): void { + const hotkey = getHotkeyService() + + this._hotkeyPressHandler = (payload) => { + const mode = this._resolveMode(payload.config) + this._enqueueAction({ type: 'press', timestamp: payload.timestamp, mode, hotkeyId: payload.config.id }) + } + + this._hotkeyReleaseHandler = (payload) => { + const mode = this._resolveMode(payload.config) + this._enqueueAction({ type: 'release', timestamp: payload.timestamp, mode, hotkeyId: payload.config.id }) + } + + this._doublePressHandler = (payload) => { + // 더블프레스 → hands-free 모드 토글 + this._enqueueAction({ type: 'press', timestamp: Date.now(), mode: 'hands-free', hotkeyId: payload.config.id }) + } + + hotkey.on('hotkey-pressed', this._hotkeyPressHandler) + hotkey.on('hotkey-released', this._hotkeyReleaseHandler) + hotkey.on('double-press', this._doublePressHandler) + + logger.info('Hotkey events connected') + } + + // ── 세션 제어 ────────────────────────────────────────── + + async startSession(mode: VoiceMode): Promise { + if (this._disposed) return + if (this._session && !this._isInTerminalState()) { + logger.warn('Session already active, ignoring startSession') + return + } + + // 세션 생성 + this._session = { + id: randomUUID(), + mode, + startedAt: Date.now(), + recognitionState: RecognitionState.PREPARING, + audioState: AudioState.IDLE, + audioBufferDurationMs: 0, + transcription: '', + processedText: null, + accidentalPress: false + } + + this._errorEmitted = false + this._sttReady = false + this._audioStarted = false + this._audioBuffer = [] + this._audioBufferBytes = 0 + + this._setRecognitionState(RecognitionState.PREPARING) + this.emit('session-started', { session: this._session }) + logger.info(`Session started: ${this._session.id} (mode: ${mode})`) + + // 이중 조건 플러시: STT 초기화 + 오디오 캡처를 병렬 시작 + const sttPromise = this._initSTT() + const audioPromise = this._startAudio() + + // 둘 다 에러여도 개별 처리하므로 allSettled + await Promise.allSettled([sttPromise, audioPromise]) + } + + async stopSession(): Promise { + if (!this._session || this._isInTerminalState()) return + + const session = this._session + const duration = Date.now() - session.startedAt + + // accidentalPress 체크 (700ms 미만) + if (duration < TIMING.MIN_AUDIO_DURATION) { + session.accidentalPress = true + logger.info(`Accidental press detected (${duration}ms < ${TIMING.MIN_AUDIO_DURATION}ms)`) + this._cancelSession('too-short') + return + } + + // 오디오 캡처 중지 + await this._stopAudio() + + // 버퍼가 있으면 STT에 전달 + if (this._audioBuffer.length > 0 && this._sttReady) { + await this._transcribe() + } else if (this._audioBuffer.length > 0 && !this._sttReady) { + // STT 아직 준비 안 됨 → tryFlushAll이 처리 + logger.info('Waiting for STT to be ready before transcribing') + // 타임아웃 설정 + setTimeout(() => { + if (this._session?.id === session.id && !this._isInTerminalState()) { + logger.warn('STT readiness timeout, cancelling session') + this._cancelSession('timeout') + } + }, TIMING.POST_RECORDING_WAIT_BUFFERED) + } else { + // 오디오 없음 + logger.warn('No audio buffer, cancelling session') + this._cancelSession('too-short') + } + } + + cancelSession(): void { + this._cancelSession('user') + } + + // ── 상태 머신 ────────────────────────────────────────── + + private _isInTerminalState(): boolean { + return TERMINAL_STATES.has(this._recognitionState) + } + + private _setRecognitionState(state: RecognitionState): void { + if (this._recognitionState === state) return + if (this._isInTerminalState() && state !== RecognitionState.IDLE) return + + const previous = this._recognitionState + this._recognitionState = state + if (this._session) { + this._session.recognitionState = state + } + + this.emit('recognition-state-changed', { previous, current: state }) + logger.debug(`RecognitionState: ${previous} → ${state}`) + } + + private _setAudioState(state: AudioState): void { + if (this._audioState === state) return + + const previous = this._audioState + this._audioState = state + if (this._session) { + this._session.audioState = state + } + + this.emit('audio-state-changed', { previous, current: state }) + logger.debug(`AudioState: ${previous} → ${state}`) + } + + // ── STT 초기화 ───────────────────────────────────────── + + private async _initSTT(): Promise { + try { + this._setRecognitionState(RecognitionState.CONNECTING) + const stt = getLocalSTTService() + const modelId = configGet('sttModelId') + + await stt.initialize(modelId) + + if (this._isInTerminalState()) return + + this._sttReady = true + this._setRecognitionState(RecognitionState.READY) + logger.info('STT ready') + + this._tryFlushAll() + } catch (error) { + if (this._isInTerminalState()) return + this._handleError( + new D3ROError( + ErrorCode.STTModelLoadFailed, + `STT initialization failed: ${error instanceof Error ? error.message : String(error)}` + ) + ) + } + } + + // ── 오디오 캡처 ──────────────────────────────────────── + + private async _startAudio(): Promise { + try { + this._setAudioState(AudioState.INITIALIZING) + const audio = getAudioCaptureService() + + // 오디오 데이터 수신 + this._audioDataHandler = (payload) => { + if (this._isInTerminalState()) return + this._audioBuffer.push(payload.buffer) + this._audioBufferBytes += payload.buffer.length + + // 버퍼 duration 업데이트 (16kHz, 16bit, mono) + const durationMs = (this._audioBufferBytes / 2 / 16000) * 1000 + if (this._session) { + this._session.audioBufferDurationMs = durationMs + } + + this._tryFlushAll() + } + + this._audioLevelHandler = (payload) => { + this.emit('audio-level', { level: payload.level }) + } + + audio.on('audio-data', this._audioDataHandler) + audio.on('audio-level', this._audioLevelHandler) + + await audio.start() + + if (this._isInTerminalState()) return + + this._audioStarted = true + this._setAudioState(AudioState.STREAMING) + logger.info('Audio capture started') + + this._tryFlushAll() + } catch (error) { + if (this._isInTerminalState()) return + this._setAudioState(AudioState.STOPPED) + this._handleError( + new D3ROError( + ErrorCode.AudioCaptureStartFailed, + `Audio capture failed: ${error instanceof Error ? error.message : String(error)}` + ) + ) + } + } + + private async _stopAudio(): Promise { + this._setAudioState(AudioState.STOPPED) + + const audio = getAudioCaptureService() + if (this._audioDataHandler) { + audio.off('audio-data', this._audioDataHandler) + this._audioDataHandler = null + } + if (this._audioLevelHandler) { + audio.off('audio-level', this._audioLevelHandler) + this._audioLevelHandler = null + } + + try { + await audio.stop() + } catch (error) { + logger.warn(`Audio stop error: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // ── 이중 조건 플러시 ─────────────────────────────────── + + private _tryFlushAll(): void { + if (!this._sttReady || !this._audioStarted) return + if (this._audioBuffer.length === 0) return + if (this._isInTerminalState()) return + + // 아직 녹음 중이면 flush 하지 않음 (stopSession에서 처리) + if (this._audioState === AudioState.STREAMING) return + + this._transcribe() + } + + // ── 전사 ─────────────────────────────────────────────── + + private async _transcribe(): Promise { + if (this._audioBuffer.length === 0) return + if (this._isInTerminalState()) return + + this._setRecognitionState(RecognitionState.RECOGNIZING) + + const merged = Buffer.concat(this._audioBuffer) + this._audioBuffer = [] + this._audioBufferBytes = 0 + + try { + const stt = getLocalSTTService() + const language = configGet('sttLanguage') + + const result: TranscriptionResult = await stt.transcribe(merged, { + language: language === 'auto' ? undefined : language + }) + + if (this._isInTerminalState()) return + + if (this._session) { + this._session.transcription = result.text + } + + this.emit('transcription-update', { text: result.text, isFinal: true }) + + // Phase 2: LLM 후처리 없이 바로 완료 + this._completeSession(result.text) + } catch (error) { + if (this._isInTerminalState()) return + this._handleError( + new D3ROError( + ErrorCode.STTTranscriptionFailed, + `Transcription failed: ${error instanceof Error ? error.message : String(error)}` + ) + ) + } + } + + // ── 세션 완료/취소 ───────────────────────────────────── + + private _completeSession(finalText: string): void { + if (!this._session) return + + this._setRecognitionState(RecognitionState.COMPLETED) + this._setAudioState(AudioState.STOPPED) + + const session = { ...this._session } + logger.info(`Session completed: "${finalText.substring(0, 50)}${finalText.length > 50 ? '...' : ''}"`) + + this.emit('session-completed', { session, finalText }) + + // IDLE로 복귀 + this._resetToIdle() + } + + private _cancelSession(reason: 'user' | 'timeout' | 'too-short'): void { + if (!this._session) return + + this._setRecognitionState(RecognitionState.CANCELLED) + this._setAudioState(AudioState.STOPPED) + + const session = { ...this._session } + logger.info(`Session cancelled: ${reason}`) + + // 오디오 정리 + this._stopAudio() + + this.emit('session-cancelled', { session, reason }) + + this._resetToIdle() + } + + private _resetToIdle(): void { + this._session = null + this._audioBuffer = [] + this._audioBufferBytes = 0 + this._sttReady = false + this._audioStarted = false + this._errorEmitted = false + + // 약간의 딜레이 후 IDLE로 전이 (UI 애니메이션용) + setTimeout(() => { + if (!this._session) { + this._setRecognitionState(RecognitionState.IDLE) + this._setAudioState(AudioState.IDLE) + } + }, 200) + } + + // ── 에러 처리 ────────────────────────────────────────── + + private _handleError(error: D3ROError): void { + if (this._errorEmitted) return + this._errorEmitted = true + + logger.error(`VoiceMode error [${error.code}]: ${error.message}`) + this._setRecognitionState(RecognitionState.ERROR) + + this._stopAudio() + + this.emit('error', { error, session: this._session ? { ...this._session } : null }) + this._resetToIdle() + } + + // ── Action Queue (이벤트 직렬화) ─────────────────────── + + private _enqueueAction(action: VoiceAction): void { + this._actionQueue.push(action) + this._processQueue() + } + + private async _processQueue(): Promise { + if (this._isProcessingQueue) return + this._isProcessingQueue = true + + try { + while (this._actionQueue.length > 0) { + const action = this._actionQueue.shift()! + await this._processAction(action) + } + } finally { + this._isProcessingQueue = false + } + } + + private async _processAction(action: VoiceAction): Promise { + try { + switch (action.type) { + case 'press': + await this._handlePress(action) + break + case 'release': + await this._handleRelease(action) + break + case 'escape': + this.cancelSession() + break + } + } catch (error) { + logger.error(`Action processing error: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private async _handlePress(action: VoiceAction): Promise { + if (action.mode === 'dictation') { + // Dictation: hold-to-talk — press로 시작 + if (!this.isActive) { + await this.startSession('dictation') + } + } else if (action.mode === 'hands-free') { + // HandsFree: toggle + if (this.isActive) { + await this.stopSession() + } else { + await this.startSession('hands-free') + } + } + } + + private async _handleRelease(action: VoiceAction): Promise { + if (action.mode === 'dictation' && this.isActive) { + // Dictation: hold-to-talk — release로 종료 (200ms 딜레이) + await new Promise((resolve) => setTimeout(resolve, 200)) + await this.stopSession() + } + // HandsFree: release 무시 + } + + // ── 유틸리티 ─────────────────────────────────────────── + + private _resolveMode(config: HotkeyConfig): VoiceMode { + if (config.id === 'voice-handsfree' || config.doublePressEnabled) { + return 'hands-free' + } + return 'dictation' + } + + // ── 종료 ─────────────────────────────────────────────── + + dispose(): void { + this._disposed = true + this._actionQueue = [] + + // 진행 중 세션 취소 + if (this._session && !this._isInTerminalState()) { + this._cancelSession('user') + } + + // 핫키 리스너 해제 + const hotkey = getHotkeyService() + if (this._hotkeyPressHandler) { + hotkey.off('hotkey-pressed', this._hotkeyPressHandler) + } + if (this._hotkeyReleaseHandler) { + hotkey.off('hotkey-released', this._hotkeyReleaseHandler) + } + if (this._doublePressHandler) { + hotkey.off('double-press', this._doublePressHandler) + } + + // 오디오 리스너 해제 + this._stopAudio() + + this._setRecognitionState(RecognitionState.DESTROYED) + this.removeAllListeners() + logger.info('VoiceModeService disposed') + } + + // ── EventEmitter 타입 오버라이드 ─────────────────────── + + override on( + event: K, + listener: VoiceModeEvents[K] + ): this { + return super.on(event, listener) + } + + override off( + event: K, + listener: VoiceModeEvents[K] + ): this { + return super.off(event, listener) + } + + override emit( + event: K, + ...args: Parameters + ): boolean { + return super.emit(event, ...args) + } +} + +// ── 싱글톤 ───────────────────────────────────────────── + +let instance: VoiceModeService | null = null + +export function getVoiceModeService(): VoiceModeService { + if (!instance) { + instance = new VoiceModeService() + } + return instance +} diff --git a/src/main/services/index.ts b/src/main/services/index.ts new file mode 100644 index 0000000..4665a7f --- /dev/null +++ b/src/main/services/index.ts @@ -0,0 +1,26 @@ +// src/main/services/index.ts — 서비스 레지스트리 + +export { initLoggerService, getLogger } from './LoggerService' +export { + initConfigService, + getConfigService, + configGet, + configSet, + configGetAll, + configReset, + onConfigChanged +} from './ConfigService' +export { + getLocalSTTService, + LocalSTTService, + STTState +} from './LocalSTTService' +export type { + TranscriptionResult, + TranscriptionSegment, + TranscribeOptions, + LocalSTTEvents +} from './LocalSTTService' +export { getHotkeyService } from './HotkeyService' +export { getVoiceModeService } from './VoiceModeService' +export { getAudioCaptureService } from './AudioCaptureService' diff --git a/src/main/types/node-record-lpcm16.d.ts b/src/main/types/node-record-lpcm16.d.ts new file mode 100644 index 0000000..9e6bbad --- /dev/null +++ b/src/main/types/node-record-lpcm16.d.ts @@ -0,0 +1,33 @@ +// Type declarations for node-record-lpcm16 (no @types package available) + +declare module 'node-record-lpcm16' { + import { ChildProcess } from 'child_process' + import { Readable } from 'stream' + + interface RecordingOptions { + sampleRate?: number + channels?: number + compress?: boolean + threshold?: number + thresholdStart?: number | null + thresholdEnd?: number | null + silence?: string + recorder?: 'sox' | 'rec' | 'arecord' + endOnSilence?: boolean + audioType?: string + device?: string + } + + interface Recording { + process: ChildProcess + stop(): void + pause(): void + resume(): void + isPaused(): boolean + stream(): Readable + } + + function record(options?: RecordingOptions): Recording + + export { record, Recording, RecordingOptions } +} diff --git a/src/main/windows/TrayManager.ts b/src/main/windows/TrayManager.ts new file mode 100644 index 0000000..2a11ceb --- /dev/null +++ b/src/main/windows/TrayManager.ts @@ -0,0 +1,55 @@ +// src/main/windows/TrayManager.ts + +import { Tray, Menu, app, nativeImage } from 'electron' +import { join } from 'path' +import { getMainWindow } from './WindowManager' +import { getLogger } from '../services/LoggerService' +import { setIsQuitting } from '../lifecycle' + +const logger = getLogger('TrayManager') + +let tray: Tray | null = null + +export function createTray(): void { + // 16x16 빈 아이콘 생성 (리소스 아이콘이 없을 때 폴백) + const icon = nativeImage.createEmpty() + tray = new Tray(icon) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '표시', + click: () => { + const mainWindow = getMainWindow() + if (mainWindow) { + mainWindow.show() + mainWindow.focus() + } + } + }, + { type: 'separator' }, + { + label: '종료', + click: () => { + setIsQuitting(true) + app.quit() + } + } + ]) + + tray.setToolTip('D3RO Voice') + tray.setContextMenu(contextMenu) + + tray.on('double-click', () => { + const mainWindow = getMainWindow() + if (mainWindow) { + mainWindow.show() + mainWindow.focus() + } + }) + + logger.info('Tray created') +} + +export function getTray(): Tray | null { + return tray +} diff --git a/src/main/windows/WindowManager.ts b/src/main/windows/WindowManager.ts new file mode 100644 index 0000000..828dbc3 --- /dev/null +++ b/src/main/windows/WindowManager.ts @@ -0,0 +1,66 @@ +// src/main/windows/WindowManager.ts + +import { BrowserWindow, shell } from 'electron' +import { join } from 'path' +import { is } from '@electron-toolkit/utils' +import { WINDOW_SIZE } from '@shared/constants' +import { getLogger } from '../services/LoggerService' +import { getIsQuitting, setIsQuitting } from '../lifecycle' +import { configGet } from '../services/ConfigService' + +const logger = getLogger('WindowManager') + +let mainWindow: BrowserWindow | null = null + +export function getMainWindow(): BrowserWindow | null { + return mainWindow +} + +export function createMainWindow(): BrowserWindow { + mainWindow = new BrowserWindow({ + width: WINDOW_SIZE.MAIN.width, + height: WINDOW_SIZE.MAIN.height, + minWidth: 800, + minHeight: 600, + show: false, + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + contextIsolation: true, + nodeIntegration: false + } + }) + + mainWindow.on('ready-to-show', () => { + mainWindow?.show() + logger.info('Main window shown') + }) + + mainWindow.on('close', (event) => { + if (!getIsQuitting() && configGet('closeToTray')) { + event.preventDefault() + mainWindow?.hide() + logger.info('Main window hidden to tray') + } + }) + + mainWindow.on('closed', () => { + mainWindow = null + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + // 개발/프로덕션 URL 로드 + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } + + logger.info('Main window created') + return mainWindow +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..b2970e7 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,164 @@ +// src/preload/index.ts +// contextBridge로 렌더러에 노출할 API 정의 + +import { contextBridge, ipcRenderer } from 'electron' +import { IPC_CHANNELS } from '@shared/ipc-channels' +import type { + AudioDevice, + SetDeviceParams, + TestDeviceParams, + TestDeviceResult, + AudioDeviceChangedEvent, + AppConfig, + ConfigGetParams, + ConfigSetParams, + ConfigResetParams, + SetThemeParams, + SetLanguageParams, + ThemeMode, + ConfigChangedEvent, + VoiceState, + VoiceMode, + AudioLevelEvent, + VoiceStateChangedEvent, + TranscriptionDeltaEvent, + TranscriptionCompleteEvent, + VoiceErrorEvent, + StartRecordingParams, + StartRecordingResult, + StopRecordingParams, + StopRecordingResult, + CancelRecordingParams, + SetVoiceModeParams, + STTStatus, + STTModel, + SetSTTModelParams, + SetSTTLanguageParams, + STTStatusChangedEvent, + HotkeyBinding, + SetHotkeyParams, + SetEnabledParams, + HotkeyTriggeredEvent, + PermissionStatus +} from '@shared/types' +import type { IPCResult } from '@shared/errors' + +type Unsubscribe = () => void + +function invoke(channel: string, ...args: unknown[]): Promise> { + return ipcRenderer.invoke(channel, ...args) +} + +function send(channel: string, ...args: unknown[]): void { + ipcRenderer.send(channel, ...args) +} + +function on(channel: string, callback: (data: T) => void): Unsubscribe { + const listener = (_event: Electron.IpcRendererEvent, data: T) => callback(data) + ipcRenderer.on(channel, listener) + return () => ipcRenderer.removeListener(channel, listener) +} + +const electronAPI = { + // ── Audio ────────────────────────────────────────────── + audio: { + getDevices: () => invoke(IPC_CHANNELS.AUDIO.GET_DEVICES), + getSelectedDevice: () => invoke(IPC_CHANNELS.AUDIO.GET_SELECTED_DEVICE), + setSelectedDevice: (params: SetDeviceParams) => + invoke(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, params), + testDevice: (params: TestDeviceParams) => + invoke(IPC_CHANNELS.AUDIO.TEST_DEVICE, params), + onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe => + on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb) + }, + + // ── Config ───────────────────────────────────────────── + config: { + get: (params: ConfigGetParams) => invoke(IPC_CHANNELS.CONFIG.GET, params), + set: (params: ConfigSetParams) => invoke(IPC_CHANNELS.CONFIG.SET, params), + getAll: () => invoke(IPC_CHANNELS.CONFIG.GET_ALL), + reset: (params: ConfigResetParams) => invoke(IPC_CHANNELS.CONFIG.RESET, params), + getTheme: () => invoke(IPC_CHANNELS.CONFIG.GET_THEME), + setTheme: (params: SetThemeParams) => invoke(IPC_CHANNELS.CONFIG.SET_THEME, params), + getLanguage: () => invoke(IPC_CHANNELS.CONFIG.GET_LANGUAGE), + setLanguage: (params: SetLanguageParams) => + invoke(IPC_CHANNELS.CONFIG.SET_LANGUAGE, params), + onChanged: (cb: (e: ConfigChangedEvent) => void): Unsubscribe => + on(IPC_CHANNELS.CONFIG.CHANGED, cb) + }, + + // ── Voice ────────────────────────────────────────────── + voice: { + startRecording: (params: StartRecordingParams) => + invoke(IPC_CHANNELS.VOICE.START_RECORDING, params), + stopRecording: (params: StopRecordingParams) => + invoke(IPC_CHANNELS.VOICE.STOP_RECORDING, params), + cancelRecording: (params: CancelRecordingParams) => + invoke(IPC_CHANNELS.VOICE.CANCEL_RECORDING, params), + getState: () => invoke(IPC_CHANNELS.VOICE.GET_STATE), + setMode: (params: SetVoiceModeParams) => + invoke(IPC_CHANNELS.VOICE.SET_MODE, params), + getMode: () => invoke(IPC_CHANNELS.VOICE.GET_MODE), + onStateChanged: (cb: (e: VoiceStateChangedEvent) => void): Unsubscribe => + on(IPC_CHANNELS.VOICE.STATE_CHANGED, cb), + onTranscriptionDelta: (cb: (e: TranscriptionDeltaEvent) => void): Unsubscribe => + on(IPC_CHANNELS.VOICE.TRANSCRIPTION_DELTA, cb), + onTranscriptionComplete: (cb: (e: TranscriptionCompleteEvent) => void): Unsubscribe => + on(IPC_CHANNELS.VOICE.TRANSCRIPTION_COMPLETE, cb), + onError: (cb: (e: VoiceErrorEvent) => void): Unsubscribe => + on(IPC_CHANNELS.VOICE.ERROR, cb), + onAudioLevel: (cb: (e: AudioLevelEvent) => void): Unsubscribe => + on(IPC_CHANNELS.VOICE.AUDIO_LEVEL, cb) + }, + + // ── STT ──────────────────────────────────────────────── + stt: { + getStatus: () => invoke(IPC_CHANNELS.STT.GET_STATUS), + getModels: () => invoke(IPC_CHANNELS.STT.GET_MODELS), + getActiveModel: () => invoke(IPC_CHANNELS.STT.GET_ACTIVE_MODEL), + setModel: (params: SetSTTModelParams) => + invoke(IPC_CHANNELS.STT.SET_MODEL, params), + getLanguage: () => invoke(IPC_CHANNELS.STT.GET_LANGUAGE), + setLanguage: (params: SetSTTLanguageParams) => + invoke(IPC_CHANNELS.STT.SET_LANGUAGE, params), + onStatusChanged: (cb: (e: STTStatusChangedEvent) => void): Unsubscribe => + on(IPC_CHANNELS.STT.STATUS_CHANGED, cb) + }, + + // ── Hotkey ───────────────────────────────────────────── + hotkey: { + getDictationShortcut: () => + invoke(IPC_CHANNELS.HOTKEY.GET_DICTATION_SHORTCUT), + setDictationShortcut: (params: SetHotkeyParams) => + invoke(IPC_CHANNELS.HOTKEY.SET_DICTATION_SHORTCUT, params), + getHandsFreeShortcut: () => + invoke(IPC_CHANNELS.HOTKEY.GET_HANDS_FREE_SHORTCUT), + setHandsFreeShortcut: (params: SetHotkeyParams) => + invoke(IPC_CHANNELS.HOTKEY.SET_HANDS_FREE_SHORTCUT, params), + isEnabled: () => invoke(IPC_CHANNELS.HOTKEY.IS_ENABLED), + setEnabled: (params: SetEnabledParams) => + invoke(IPC_CHANNELS.HOTKEY.SET_ENABLED, params), + onTriggered: (cb: (e: HotkeyTriggeredEvent) => void): Unsubscribe => + on(IPC_CHANNELS.HOTKEY.TRIGGERED, cb) + }, + + // ── Window ───────────────────────────────────────────── + window: { + minimize: () => send(IPC_CHANNELS.WINDOW.MINIMIZE), + maximize: () => send(IPC_CHANNELS.WINDOW.MAXIMIZE), + close: () => send(IPC_CHANNELS.WINDOW.CLOSE), + isMaximized: () => invoke(IPC_CHANNELS.WINDOW.IS_MAXIMIZED) + }, + + // ── System ───────────────────────────────────────────── + system: { + getPlatform: () => invoke(IPC_CHANNELS.SYSTEM.GET_PLATFORM), + getVersion: () => invoke(IPC_CHANNELS.SYSTEM.GET_VERSION), + checkMicPermission: () => + invoke(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION) + } +} as const + +contextBridge.exposeInMainWorld('electronAPI', electronAPI) + +export type ElectronAPI = typeof electronAPI diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..9fa93e7 --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,26 @@ +// src/renderer/App.tsx — 루트 컴포넌트 + +import { useState, useMemo } from 'react' +import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material' +import { lightTheme, darkTheme } from './theme' +import { AppLayout } from './components/AppLayout' +import type { ThemeMode } from '@shared/types' + +export function App(): React.ReactElement { + const [themeMode] = useState('auto') + const prefersDark = useMediaQuery('(prefers-color-scheme: dark)') + + const theme = useMemo(() => { + if (themeMode === 'auto') { + return prefersDark ? darkTheme : lightTheme + } + return themeMode === 'dark' ? darkTheme : lightTheme + }, [themeMode, prefersDark]) + + return ( + + + + + ) +} diff --git a/src/renderer/components/AppLayout.tsx b/src/renderer/components/AppLayout.tsx new file mode 100644 index 0000000..4695730 --- /dev/null +++ b/src/renderer/components/AppLayout.tsx @@ -0,0 +1,110 @@ +// src/renderer/components/AppLayout.tsx + +import { useState } from 'react' +import { + Box, + Drawer, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Divider, + Typography, + Chip +} from '@mui/material' +import DashboardIcon from '@mui/icons-material/Dashboard' +import HistoryIcon from '@mui/icons-material/History' +import MenuBookIcon from '@mui/icons-material/MenuBook' +import SettingsIcon from '@mui/icons-material/Settings' +import { DashboardPage } from '../pages/DashboardPage' + +type Route = 'dashboard' | 'history' | 'dictionary' + +const DRAWER_WIDTH = 240 + +const NAV_ITEMS: Array<{ route: Route; label: string; icon: React.ReactElement }> = [ + { route: 'dashboard', label: 'Dashboard', icon: }, + { route: 'history', label: 'History', icon: }, + { route: 'dictionary', label: 'Dictionary', icon: } +] + +export function AppLayout(): React.ReactElement { + const [currentRoute, setCurrentRoute] = useState('dashboard') + + return ( + + {/* Sidebar Drawer */} + + {/* Header */} + + + D3RO Voice + + + + + + + {/* Navigation */} + + {NAV_ITEMS.map((item) => ( + setCurrentRoute(item.route)} + sx={{ my: 0.5 }} + > + {item.icon} + + + ))} + + + + + {/* Bottom */} + + + + + + + + + + + {/* Content Area */} + + {currentRoute === 'dashboard' && } + {currentRoute === 'history' && ( + + History (Phase 5) + + )} + {currentRoute === 'dictionary' && ( + + Dictionary (Phase 5) + + )} + + + ) +} diff --git a/src/renderer/electron.d.ts b/src/renderer/electron.d.ts new file mode 100644 index 0000000..8cf3f05 --- /dev/null +++ b/src/renderer/electron.d.ts @@ -0,0 +1,8 @@ +// src/renderer/electron.d.ts +import type { ElectronAPI } from '../preload/index' + +declare global { + interface Window { + electronAPI: ElectronAPI + } +} diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..8af3755 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + D3RO Voice + + +
+ + + diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx new file mode 100644 index 0000000..4417244 --- /dev/null +++ b/src/renderer/main.tsx @@ -0,0 +1,14 @@ +// src/renderer/main.tsx — 렌더러 진입점 + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './App' + +const root = document.getElementById('root') +if (root) { + createRoot(root).render( + + + + ) +} diff --git a/src/renderer/pages/DashboardPage.tsx b/src/renderer/pages/DashboardPage.tsx new file mode 100644 index 0000000..fb62ae3 --- /dev/null +++ b/src/renderer/pages/DashboardPage.tsx @@ -0,0 +1,67 @@ +// src/renderer/pages/DashboardPage.tsx + +import { Box, Card, CardContent, Typography, Grid } from '@mui/material' +import MicIcon from '@mui/icons-material/Mic' +import TimerIcon from '@mui/icons-material/Timer' +import TextFieldsIcon from '@mui/icons-material/TextFields' +import TodayIcon from '@mui/icons-material/Today' + +interface StatCardProps { + title: string + value: string + icon: React.ReactElement +} + +function StatCard({ title, value, icon }: StatCardProps): React.ReactElement { + return ( + + + + {icon} + + {title} + + + {value} + + + ) +} + +export function DashboardPage(): React.ReactElement { + return ( + + + Dashboard + + + + + } /> + + + } /> + + + } /> + + + } /> + + + + + + Recent Sessions + + + + + No sessions yet. Press the hotkey to start recording. + + + + + + ) +} diff --git a/src/renderer/theme.ts b/src/renderer/theme.ts new file mode 100644 index 0000000..efb234b --- /dev/null +++ b/src/renderer/theme.ts @@ -0,0 +1,148 @@ +// src/renderer/theme.ts — MUI 7 테마 정의 (설계서 03 기반) + +import { createTheme, type ThemeOptions } from '@mui/material/styles' + +const commonOptions: ThemeOptions = { + typography: { + fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif' + ].join(','), + h4: { fontWeight: 600, fontSize: '1.5rem' }, + h5: { fontWeight: 600, fontSize: '1.25rem' }, + h6: { fontWeight: 600, fontSize: '1rem' }, + subtitle1: { fontWeight: 500 }, + body1: { fontSize: '0.9375rem' }, + body2: { fontSize: '0.8125rem' }, + button: { textTransform: 'none' as const, fontWeight: 500 } + }, + shape: { + borderRadius: 12 + }, + components: { + MuiButton: { + defaultProps: { + disableElevation: true + }, + styleOverrides: { + root: { + textTransform: 'none', + fontWeight: 500, + borderRadius: 8, + padding: '8px 16px' + } + } + }, + MuiCard: { + defaultProps: { + elevation: 0 + }, + styleOverrides: { + root: { + borderRadius: 12, + border: '1px solid' + } + } + }, + MuiDrawer: { + styleOverrides: { + paper: { + width: 240, + borderRight: 'none' + } + } + }, + MuiListItemButton: { + styleOverrides: { + root: { + borderRadius: 8, + marginLeft: 8, + marginRight: 8 + } + } + }, + MuiTextField: { + defaultProps: { + size: 'small', + variant: 'outlined' + } + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 6, + fontWeight: 500 + } + } + } + } +} + +export const lightTheme = createTheme({ + ...commonOptions, + palette: { + mode: 'light', + primary: { + main: 'rgb(31, 93, 242)', + light: 'rgb(71, 133, 255)', + dark: 'rgb(20, 65, 180)', + contrastText: '#FFFFFF' + }, + secondary: { + main: 'rgb(108, 117, 125)', + light: 'rgb(173, 181, 189)', + dark: 'rgb(73, 80, 87)' + }, + background: { + default: '#F9F9F9', + paper: '#FFFFFF' + }, + text: { + primary: 'rgba(0, 0, 0, 0.87)', + secondary: 'rgba(0, 0, 0, 0.6)' + }, + divider: 'rgba(0, 0, 0, 0.08)', + error: { main: '#D32F2F' }, + success: { main: '#2E7D32' }, + warning: { main: '#ED6C02' } + } +}) + +export const darkTheme = createTheme({ + ...commonOptions, + palette: { + mode: 'dark', + primary: { + main: 'rgb(71, 133, 255)', + light: 'rgb(120, 170, 255)', + dark: 'rgb(31, 93, 242)', + contrastText: '#FFFFFF' + }, + secondary: { + main: 'rgb(173, 181, 189)', + light: 'rgb(206, 212, 218)', + dark: 'rgb(108, 117, 125)' + }, + background: { + default: '#121212', + paper: '#1E1E1E' + }, + text: { + primary: 'rgba(255, 255, 255, 0.87)', + secondary: 'rgba(255, 255, 255, 0.6)' + }, + divider: 'rgba(255, 255, 255, 0.08)', + error: { main: '#EF5350' }, + success: { main: '#4CAF50' }, + warning: { main: '#FFA726' } + } +}) + +export function getTheme(mode: 'light' | 'dark') { + return mode === 'dark' ? darkTheme : lightTheme +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..fe52583 --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1,58 @@ +// src/shared/constants.ts + +/** 타이밍 상수 (Speakly 리버스엔지니어링 기반) */ +export const TIMING = { + /** 더블프레스 감지 간격 (ms) */ + DOUBLE_PRESS_DURATION: 300, + + /** 최소 녹음 시간 (ms) — 이하 자동 취소 */ + MIN_AUDIO_DURATION: 700, + + /** 녹음 후 STT 대기 시간 (ms) */ + POST_RECORDING_WAIT: 4000, + + /** 녹음 후 STT 대기 (버퍼 있을 때) (ms) */ + POST_RECORDING_WAIT_BUFFERED: 6000, + + /** STT 아이들 타임아웃 (ms) */ + STT_IDLE_TIMEOUT: 30000, + + /** 절대 최대 대기 시간 (ms) */ + ABSOLUTE_MAX_WAIT: 120000, + + /** 오디오 레벨 전송 간격 (ms) */ + AUDIO_LEVEL_INTERVAL: 100, + + /** 서비스 종료 타임아웃 (ms) */ + SERVICE_DESTROY_TIMEOUT: 3000, + + /** 사이드카 헬스체크 지연 (ms) */ + SIDECAR_HEALTH_DELAY: 3000, + + /** LLM 요청 타임아웃 (ms) */ + LLM_REQUEST_TIMEOUT: 30000 +} as const + +/** 웨이브 바 상수 (RecordingTip) */ +export const WAVE_BAR = { + COUNT: 9, + ANIMATION_INTERVAL: 100, + + /** 코사인 분포 가중치 (Speakly 패턴) */ + COS_WEIGHTS: Array.from({ length: 9 }, (_, i) => Math.cos((i - 4) * (Math.PI / 9))) +} as const + +/** 오디오 포맷 */ +export const AUDIO_FORMAT = { + SAMPLE_RATE: 16000, + CHANNELS: 1, + BIT_DEPTH: 16, + BYTES_PER_SAMPLE: 2 +} as const + +/** 윈도우 크기 */ +export const WINDOW_SIZE = { + MAIN: { width: 1104, height: 816 }, + RECORDING_TIP: { width: 280, height: 80 }, + RESULT_POPUP: { width: 400, height: 200 } +} as const diff --git a/src/shared/errors.ts b/src/shared/errors.ts new file mode 100644 index 0000000..94d8552 --- /dev/null +++ b/src/shared/errors.ts @@ -0,0 +1,162 @@ +// src/shared/errors.ts + +export enum ErrorCode { + // === Success === + Success = 0, + + // === STT (100-199) === + STTEngineNotInstalled = 100, + STTModelNotFound = 101, + STTModelNotLoaded = 102, + STTModelLoadFailed = 103, + STTModelDownloadFailed = 104, + STTModelDownloadCancelled = 105, + STTTranscriptionFailed = 110, + STTTranscriptionTimeout = 111, + STTTranscriptionCancelled = 112, + STTNoAudioData = 113, + STTAudioTooShort = 114, + STTLanguageNotSupported = 120, + STTSidecarSpawnFailed = 130, + STTSidecarCrashed = 131, + STTSidecarCommunicationFailed = 132, + STTGPUNotAvailable = 140, + + // === TTS (200-299) === + TTSEngineNotInstalled = 200, + TTSVoiceNotFound = 201, + TTSVoiceNotLoaded = 202, + TTSVoiceLoadFailed = 203, + TTSVoiceDownloadFailed = 204, + TTSSynthesisFailed = 210, + TTSPlaybackFailed = 211, + TTSPlaybackInterrupted = 212, + TTSTextTooLong = 220, + TTSTextEmpty = 221, + + // === LLM / Ollama (300-399) === + LLMServerUnreachable = 300, + LLMServerConnectionFailed = 301, + LLMServerTimeout = 302, + LLMModelNotFound = 310, + LLMModelNotLoaded = 311, + LLMModelLoadFailed = 312, + LLMModelPullFailed = 313, + LLMModelPullCancelled = 314, + LLMProcessingFailed = 320, + LLMProcessingTimeout = 321, + LLMProcessingCancelled = 322, + LLMResponseParseFailed = 323, + LLMInvalidAction = 330, + LLMPromptTooLong = 331, + + // === Audio (400-499) === + AudioDeviceNotFound = 400, + AudioDeviceAccessDenied = 401, + AudioDeviceBusy = 402, + AudioCaptureStartFailed = 410, + AudioCaptureStopFailed = 411, + AudioCaptureFailed = 412, + AudioNoPermission = 420, + AudioStreamError = 430, + AudioBufferOverflow = 431, + + // === Hotkey (500-599) === + HotkeyRegistrationFailed = 500, + HotkeyConflict = 501, + HotkeySystemReserved = 502, + HotkeyHookInitFailed = 510, + HotkeyHookCrashed = 511, + + // === TextInsert (600-699) === + TextInsertFailed = 600, + TextInsertClipboardSaveFailed = 601, + TextInsertClipboardRestoreFailed = 602, + TextInsertKeySimulationFailed = 603, + TextInsertNoActiveWindow = 610, + TextInsertTargetAppNotResponding = 611, + + // === History / Dictionary / DB (700-799) === + DBOpenFailed = 700, + DBMigrationFailed = 701, + DBQueryFailed = 702, + DBWriteFailed = 703, + HistoryNotFound = 710, + HistoryExportFailed = 711, + DictionaryNotFound = 720, + DictionaryDuplicate = 721, + DictionaryImportFailed = 722, + DictionaryExportFailed = 723, + DictionaryImportInvalidFormat = 724, + + // === Config (800-899) === + ConfigReadFailed = 800, + ConfigWriteFailed = 801, + ConfigInvalidValue = 802, + ConfigKeyNotFound = 803, + ConfigResetFailed = 804, + ConfigMigrationFailed = 810, + + // === System / Window (900-999) === + WindowCreationFailed = 900, + WindowNotFound = 901, + TrayCreationFailed = 910, + NotificationFailed = 920, + PermissionDenied = 930, + ExternalOpenFailed = 940, + SoundPlayFailed = 950, + AppAlreadyRunning = 960, + UnknownError = 999 +} + +/** + * D3RO-VOICE 표준 에러 객체 (Speakly NXError 패턴) + */ +export class D3ROError extends Error { + readonly code: ErrorCode + readonly details?: Record + + constructor(code: ErrorCode, message: string, details?: Record) { + super(message) + this.name = 'D3ROError' + this.code = code + this.details = details + } + + toJSON(): D3ROErrorJSON { + return { + code: this.code, + message: this.message, + details: this.details + } + } + + static fromJSON(json: D3ROErrorJSON): D3ROError { + return new D3ROError(json.code, json.message, json.details) + } +} + +export interface D3ROErrorJSON { + code: ErrorCode + message: string + details?: Record +} + +/** + * IPC 핸들러에서 사용하는 표준 응답 래퍼. + */ +export type IPCResult = + | { success: true; data: T } + | { success: false; error: D3ROErrorJSON } + +export function ipcSuccess(data: T): IPCResult { + return { success: true, data } +} + +export function ipcError( + code: ErrorCode, + message: string, + details?: Record +): IPCResult { + return { success: false, error: { code, message, details } } +} diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts new file mode 100644 index 0000000..c5ae296 --- /dev/null +++ b/src/shared/ipc-channels.ts @@ -0,0 +1,172 @@ +// src/shared/ipc-channels.ts +// IPC 채널명 중앙 정의 — 모든 채널명은 이 파일에서만 정의한다. + +export const IPC_CHANNELS = { + VOICE: { + START_RECORDING: 'voice:startRecording', + STOP_RECORDING: 'voice:stopRecording', + CANCEL_RECORDING: 'voice:cancelRecording', + GET_STATE: 'voice:getState', + SET_MODE: 'voice:setMode', + GET_MODE: 'voice:getMode', + // Main → Renderer events + STATE_CHANGED: 'voice:stateChanged', + TRANSCRIPTION_DELTA: 'voice:transcriptionDelta', + TRANSCRIPTION_COMPLETE: 'voice:transcriptionComplete', + ERROR: 'voice:error', + AUDIO_LEVEL: 'voice:audioLevel' + }, + + AUDIO: { + GET_DEVICES: 'audio:getDevices', + GET_SELECTED_DEVICE: 'audio:getSelectedDevice', + SET_SELECTED_DEVICE: 'audio:setSelectedDevice', + TEST_DEVICE: 'audio:testDevice', + // Main → Renderer events + DEVICE_CHANGED: 'audio:deviceChanged' + }, + + STT: { + GET_STATUS: 'stt:getStatus', + GET_MODELS: 'stt:getModels', + GET_ACTIVE_MODEL: 'stt:getActiveModel', + SET_MODEL: 'stt:setModel', + DOWNLOAD_MODEL: 'stt:downloadModel', + CANCEL_DOWNLOAD: 'stt:cancelDownload', + GET_LANGUAGE: 'stt:getLanguage', + SET_LANGUAGE: 'stt:setLanguage', + // Main → Renderer events + STATUS_CHANGED: 'stt:statusChanged', + DOWNLOAD_PROGRESS: 'stt:downloadProgress' + }, + + TTS: { + SPEAK: 'tts:speak', + STOP: 'tts:stop', + GET_VOICES: 'tts:getVoices', + GET_ACTIVE_VOICE: 'tts:getActiveVoice', + SET_VOICE: 'tts:setVoice', + GET_STATUS: 'tts:getStatus', + DOWNLOAD_VOICE: 'tts:downloadVoice', + // Main → Renderer events + STATUS_CHANGED: 'tts:statusChanged', + SPEAKING_STATE_CHANGED: 'tts:speakingStateChanged' + }, + + LLM: { + GET_STATUS: 'llm:getStatus', + GET_MODELS: 'llm:getModels', + GET_ACTIVE_MODEL: 'llm:getActiveModel', + SET_MODEL: 'llm:setModel', + PROCESS: 'llm:process', + CANCEL_PROCESS: 'llm:cancelProcess', + GET_SERVER_URL: 'llm:getServerUrl', + SET_SERVER_URL: 'llm:setServerUrl', + PULL_MODEL: 'llm:pullModel', + // Main → Renderer events + STATUS_CHANGED: 'llm:statusChanged', + PROCESS_PROGRESS: 'llm:processProgress', + PULL_PROGRESS: 'llm:pullProgress' + }, + + HOTKEY: { + GET_DICTATION_SHORTCUT: 'hotkey:getDictationShortcut', + SET_DICTATION_SHORTCUT: 'hotkey:setDictationShortcut', + GET_HANDS_FREE_SHORTCUT: 'hotkey:getHandsFreeShortcut', + SET_HANDS_FREE_SHORTCUT: 'hotkey:setHandsFreeShortcut', + GET_COMMAND_SHORTCUT: 'hotkey:getCommandShortcut', + SET_COMMAND_SHORTCUT: 'hotkey:setCommandShortcut', + IS_ENABLED: 'hotkey:isEnabled', + SET_ENABLED: 'hotkey:setEnabled', + START_RECORDING: 'hotkey:startRecording', + STOP_RECORDING: 'hotkey:stopRecording', + // Main → Renderer events + TRIGGERED: 'hotkey:triggered', + RECORDING_RESULT: 'hotkey:recordingResult' + }, + + CONFIG: { + GET: 'config:get', + SET: 'config:set', + GET_ALL: 'config:getAll', + RESET: 'config:reset', + GET_THEME: 'config:getTheme', + SET_THEME: 'config:setTheme', + GET_LANGUAGE: 'config:getLanguage', + SET_LANGUAGE: 'config:setLanguage', + GET_AUTO_LAUNCH: 'config:getAutoLaunch', + SET_AUTO_LAUNCH: 'config:setAutoLaunch', + GET_CLOSE_TO_TRAY: 'config:getCloseToTray', + SET_CLOSE_TO_TRAY: 'config:setCloseToTray', + // Main → Renderer events + CHANGED: 'config:changed' + }, + + HISTORY: { + GET_ALL: 'history:getAll', + GET_BY_ID: 'history:getById', + DELETE: 'history:delete', + DELETE_ALL: 'history:deleteAll', + SEARCH: 'history:search', + EXPORT: 'history:export', + // Main → Renderer events + ADDED: 'history:added' + }, + + DICTIONARY: { + GET_ALL: 'dictionary:getAll', + ADD: 'dictionary:add', + UPDATE: 'dictionary:update', + DELETE: 'dictionary:delete', + IMPORT: 'dictionary:import', + EXPORT: 'dictionary:export', + SEARCH: 'dictionary:search' + }, + + WINDOW: { + MINIMIZE: 'window:minimize', + MAXIMIZE: 'window:maximize', + CLOSE: 'window:close', + IS_MAXIMIZED: 'window:isMaximized', + SHOW_RECORDING_TIP: 'window:showRecordingTip', + HIDE_RECORDING_TIP: 'window:hideRecordingTip', + SHOW_RESULT_POPUP: 'window:showResultPopup', + HIDE_RESULT_POPUP: 'window:hideResultPopup', + TIP_MEASURED: 'window:tipMeasured', + // Main → Renderer events + TIP_STATE_CHANGED: 'window:tipStateChanged', + TIP_PREPARE: 'window:tipPrepare', + TIP_SHOW: 'window:tipShow' + }, + + SYSTEM: { + GET_PLATFORM: 'system:getPlatform', + GET_VERSION: 'system:getVersion', + CHECK_MIC_PERMISSION: 'system:checkMicPermission', + REQUEST_MIC_PERMISSION: 'system:requestMicPermission', + SHOW_NOTIFICATION: 'system:showNotification', + OPEN_EXTERNAL: 'system:openExternal', + GET_ACTIVE_APP: 'system:getActiveApp', + INSERT_TEXT: 'system:insertText', + PLAY_SOUND: 'system:playSound', + SET_SOUND_ENABLED: 'system:setSoundEnabled', + IS_SOUND_ENABLED: 'system:isSoundEnabled' + }, + + STATS: { + GET_SUMMARY: 'stats:getSummary', + GET_DAILY: 'stats:getDaily', + GET_WEEKLY: 'stats:getWeekly', + // Main → Renderer events + UPDATED: 'stats:updated' + } +} as const + +// 타입 유틸리티: 채널명 유니온 추출 +type NestedValues = T extends Record + ? V extends string + ? V + : NestedValues + : never + +export type IPCChannel = NestedValues diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..77c07c7 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,658 @@ +// src/shared/types.ts +// 모든 IPC 파라미터/반환 타입 정의 + +// ============================================================ +// Common +// ============================================================ + +export type ThemeMode = 'light' | 'dark' | 'auto' + +export type VoiceMode = 'dictation' | 'hands-free' + +export type PermissionStatus = 'granted' | 'denied' | 'unknown' + +// ============================================================ +// Voice (음성 오케스트레이션) +// ============================================================ + +export enum RecognitionState { + IDLE = 'idle', + PREPARING = 'preparing', + CONNECTING = 'connecting', + READY = 'ready', + RECOGNIZING = 'recognizing', + COMPLETED = 'completed', + CANCELLED = 'cancelled', + ERROR = 'error', + DESTROYED = 'destroyed' +} + +export enum AudioState { + IDLE = 'idle', + INITIALIZING = 'initializing', + STREAMING = 'streaming', + STOPPED = 'stopped' +} + +export interface VoiceState { + recognitionState: RecognitionState + audioState: AudioState + mode: VoiceMode + sessionId: string | null + recordingStartedAt: number | null +} + +export interface StartRecordingParams { + sessionId?: string + deviceId?: string +} + +export interface StartRecordingResult { + sessionId: string +} + +export interface StopRecordingParams { + sessionId: string +} + +export interface StopRecordingResult { + sessionId: string + text: string + durationMs: number +} + +export interface CancelRecordingParams { + sessionId: string +} + +export interface SetVoiceModeParams { + mode: VoiceMode +} + +// Voice events (Main → Renderer) + +export interface VoiceStateChangedEvent { + previousState: RecognitionState + currentState: RecognitionState + audioState: AudioState + sessionId: string | null +} + +export interface TranscriptionDeltaEvent { + sessionId: string + text: string + delta: string + isFinal: boolean +} + +export interface TranscriptionCompleteEvent { + sessionId: string + text: string + durationMs: number + language: string +} + +export interface VoiceErrorEvent { + sessionId: string | null + errorCode: number + message: string +} + +export interface AudioLevelEvent { + level: number +} + +// ============================================================ +// Audio (디바이스 & 캡처) +// ============================================================ + +export interface AudioDevice { + deviceId: string + label: string + isDefault: boolean +} + +export interface SetDeviceParams { + deviceId: string +} + +export interface TestDeviceParams { + deviceId: string + durationMs?: number +} + +export interface TestDeviceResult { + averageLevel: number + peakLevel: number + hasAudio: boolean +} + +export interface AudioDeviceChangedEvent { + devices: AudioDevice[] + type: 'added' | 'removed' | 'default-changed' +} + +// ============================================================ +// STT (로컬 Whisper) +// ============================================================ + +export enum STTEngineState { + NOT_INSTALLED = 'not-installed', + DOWNLOADING = 'downloading', + LOADING = 'loading', + READY = 'ready', + PROCESSING = 'processing', + ERROR = 'error' +} + +export interface STTStatus { + engineState: STTEngineState + activeModel: string | null + engineVersion: string | null + gpuAccelerated: boolean +} + +export interface STTModel { + id: string + name: string + sizeBytes: number + downloaded: boolean + languages: string[] + accuracy: number + speed: number +} + +export interface SetSTTModelParams { + modelId: string +} + +export interface DownloadModelParams { + modelId: string +} + +export interface SetSTTLanguageParams { + language: string +} + +export interface STTStatusChangedEvent { + status: STTStatus +} + +export interface DownloadProgressEvent { + modelId: string + percent: number + downloadedBytes: number + totalBytes: number + bytesPerSecond: number +} + +// ============================================================ +// TTS (로컬 TTS) +// ============================================================ + +export enum TTSEngineState { + NOT_INSTALLED = 'not-installed', + LOADING = 'loading', + READY = 'ready', + SPEAKING = 'speaking', + ERROR = 'error' +} + +export interface TTSStatus { + engineState: TTSEngineState + activeVoice: string | null + engineVersion: string | null +} + +export interface TTSVoice { + id: string + name: string + language: string + gender: 'male' | 'female' | 'neutral' + downloaded: boolean + sizeBytes: number +} + +export interface TTSSpeakParams { + text: string + voiceId?: string + speed?: number +} + +export interface TTSSpeakResult { + durationMs: number +} + +export interface SetTTSVoiceParams { + voiceId: string +} + +export interface DownloadVoiceParams { + voiceId: string +} + +export interface TTSStatusChangedEvent { + status: TTSStatus +} + +export interface SpeakingStateChangedEvent { + isSpeaking: boolean + text: string +} + +// ============================================================ +// LLM (Ollama) +// ============================================================ + +export enum LLMConnectionState { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + ERROR = 'error' +} + +export interface LLMStatus { + connectionState: LLMConnectionState + serverUrl: string + activeModel: string | null + serverVersion: string | null +} + +export interface LLMModel { + id: string + name: string + sizeBytes: number + parameterSize: string + quantization: string + modifiedAt: string +} + +export type LLMAction = 'refine' | 'translate' | 'summarize' | 'expand' | 'grammar' | 'custom' + +export interface LLMProcessParams { + text: string + action: LLMAction + targetLanguage?: string + customPrompt?: string + modelId?: string +} + +export interface LLMProcessResult { + originalText: string + processedText: string + action: LLMAction + processingTimeMs: number + tokenCount: number +} + +export interface SetLLMModelParams { + modelId: string +} + +export interface SetServerUrlParams { + url: string +} + +export interface PullModelParams { + modelName: string +} + +export interface LLMStatusChangedEvent { + status: LLMStatus +} + +export interface LLMProcessProgressEvent { + text: string + token: string + done: boolean +} + +export interface LLMPullProgressEvent { + modelName: string + status: string + percent: number + downloadedBytes: number + totalBytes: number +} + +// ============================================================ +// Hotkey (핫키) +// ============================================================ + +export interface HotkeyBinding { + keyCode: number + ctrl: boolean + alt: boolean + shift: boolean + meta: boolean + displayLabel: string +} + +export interface SetHotkeyParams { + binding: HotkeyBinding +} + +export interface SetEnabledParams { + enabled: boolean +} + +export type HotkeyAction = 'dictation' | 'hands-free' | 'command' + +export interface HotkeyTriggeredEvent { + action: HotkeyAction + type: 'pressed' | 'released' + isDoublePress: boolean +} + +export interface HotkeyRecordingResultEvent { + binding: HotkeyBinding | null + conflictReason: string | null +} + +// ============================================================ +// Config (설정) +// ============================================================ + +export interface AppConfig { + theme: ThemeMode + language: string + closeToTray: boolean + autoLaunch: boolean + soundEnabled: boolean + selectedDeviceId: string | null + sttModelId: string + sttLanguage: string + ttsVoiceId: string | null + ttsSpeed: number + ollamaServerUrl: string + llmModelId: string | null + defaultLLMAction: LLMAction + dictationShortcut: HotkeyBinding + handsFreeShortcut: HotkeyBinding + commandShortcut: HotkeyBinding + hotkeyEnabled: boolean + insertMethod: 'clipboard' | 'keyboard' + autoInsert: boolean + maxHistoryEntries: number +} + +export interface ConfigGetParams { + key: keyof AppConfig +} + +export interface ConfigSetParams { + key: keyof AppConfig + value: AppConfig[keyof AppConfig] +} + +export interface ConfigResetParams { + key?: keyof AppConfig +} + +export interface SetThemeParams { + theme: ThemeMode +} + +export interface SetLanguageParams { + language: string +} + +export interface SetAutoLaunchParams { + enabled: boolean +} + +export interface SetCloseToTrayParams { + enabled: boolean +} + +export interface ConfigChangedEvent { + key: keyof AppConfig + value: AppConfig[keyof AppConfig] + previousValue: AppConfig[keyof AppConfig] +} + +// ============================================================ +// History (히스토리) +// ============================================================ + +export interface HistoryEntry { + id: string + originalText: string + polishedText: string | null + focusedApp: string | null + focusedAppName: string | null + focusedAppWindowTitle: string | null + mode: 'dictation' | 'translate' | 'command' + status: 'completed' | 'cancelled' | 'error' + errorCode: string | null + audioLocalPath: string | null + duration: number + detectedLanguage: string | null + micDevice: string | null + wordCount: number + sttModel: string | null + llmModel: string | null + sttLatencyMs: number | null + llmLatencyMs: number | null + createdAt: number + updatedAt: number + appVersion: string +} + +export interface HistoryQueryParams { + page: number + pageSize: number + sortBy?: 'createdAt' | 'durationMs' | 'wordCount' + sortOrder?: 'asc' | 'desc' +} + +export interface HistoryPage { + entries: HistoryEntry[] + total: number + page: number + pageSize: number + totalPages: number +} + +export interface HistoryGetByIdParams { + id: string +} + +export interface HistoryDeleteParams { + id: string +} + +export interface HistorySearchParams { + query: string + page: number + pageSize: number +} + +export interface HistoryExportParams { + format: 'json' | 'csv' + from?: string + to?: string +} + +// ============================================================ +// Dictionary (사전) +// ============================================================ + +export interface DictionaryEntry { + id: string + word: string + pronunciation: string | null + category: 'user' | 'auto' | 'technical' + usageCount: number + lastUsedAt: number | null + createdAt: number + updatedAt: number +} + +export interface DictionaryQueryParams { + page: number + pageSize: number + sortBy?: 'word' | 'category' | 'usageCount' | 'createdAt' + sortOrder?: 'asc' | 'desc' +} + +export interface DictionaryPage { + entries: DictionaryEntry[] + total: number + page: number + pageSize: number + totalPages: number +} + +export interface DictionaryAddParams { + word: string + pronunciation?: string + category?: 'user' | 'auto' | 'technical' +} + +export interface DictionaryUpdateParams { + id: string + word?: string + pronunciation?: string + category?: 'user' | 'auto' | 'technical' +} + +export interface DictionaryDeleteParams { + id: string +} + +export interface DictionaryImportParams { + filePath: string + format: 'json' | 'csv' +} + +export interface DictionaryImportResult { + imported: number + skipped: number + errors: number +} + +export interface DictionaryExportParams { + format: 'json' | 'csv' +} + +export interface DictionarySearchParams { + query: string + page: number + pageSize: number +} + +// ============================================================ +// Window (윈도우 제어) +// ============================================================ + +export type RecordingTipState = 'opening' | 'recording' | 'thinking' | 'result' | 'error' + +export interface ShowRecordingTipParams { + state: RecordingTipState + text?: string + errorMessage?: string +} + +export interface ShowResultPopupParams { + text: string + autoHideMs?: number +} + +export interface TipMeasuredParams { + width: number + height: number +} + +export interface TipStateChangedEvent { + state: RecordingTipState + text?: string + errorMessage?: string +} + +export interface TipPrepareEvent { + state: RecordingTipState + text?: string +} + +export interface TipShowEvent { + state: RecordingTipState +} + +// ============================================================ +// System (시스템) +// ============================================================ + +export interface ActiveAppInfo { + name: string + title: string + pid: number +} + +export interface ShowNotificationParams { + title: string + body: string + type?: 'info' | 'warning' | 'error' +} + +export interface OpenExternalParams { + url: string +} + +export interface InsertTextParams { + text: string + method?: 'clipboard' | 'keyboard' +} + +export interface InsertTextResult { + success: boolean + insertedLength: number +} + +export type SoundEffect = + | 'recording-start' + | 'recording-stop' + | 'transcription-complete' + | 'error' + | 'notification' + +export interface PlaySoundParams { + sound: SoundEffect +} + +export interface SetSoundEnabledParams { + enabled: boolean +} + +// ============================================================ +// Stats (통계) +// ============================================================ + +export interface StatsSummary { + totalRecordingTimeMs: number + totalWordCount: number + totalSessionCount: number + todayRecordingTimeMs: number + todayWordCount: number + todaySessionCount: number + streakDays: number +} + +export interface StatsQueryParams { + from: string + to: string +} + +export interface DailyStats { + date: string + recordingTimeMs: number + wordCount: number + sessionCount: number +} + +export interface WeeklyStats { + weekStart: string + recordingTimeMs: number + wordCount: number + sessionCount: number +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..155ebaa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..666444e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", + "compilerOptions": { + "composite": true, + "outDir": "./out", + "baseUrl": ".", + "paths": { + "@shared/*": ["src/shared/*"] + }, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/main/**/*", + "src/preload/**/*", + "src/shared/**/*", + "electron.vite.config.ts" + ] +} diff --git a/tsconfig.web.json b/tsconfig.web.json new file mode 100644 index 0000000..b42c268 --- /dev/null +++ b/tsconfig.web.json @@ -0,0 +1,24 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "compilerOptions": { + "composite": true, + "outDir": "./out", + "baseUrl": ".", + "paths": { + "@shared/*": ["src/shared/*"] + }, + "jsx": "react-jsx", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/renderer/**/*", + "src/shared/**/*" + ] +}