Compare commits
29 Commits
350e405fac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 393e26e8de | |||
| b309dca688 | |||
| 5a817e6646 | |||
| f78c022f75 | |||
| 7cf48246f2 | |||
| df2cf549c9 | |||
| 669bfac722 | |||
| 677ef35ff7 | |||
| fc34989eaa | |||
| 0cb6dd418f | |||
| ab60db0dc5 | |||
| 08e5abe165 | |||
| 70344bcd98 | |||
| ea46ad71bf | |||
| aaea414d5a | |||
| 6c1b8e95c8 | |||
| 2c322f5ab1 | |||
| 48c8444b3f | |||
| 0720505ef6 | |||
| dd55be6f5b | |||
| 1e4670cd26 | |||
| b786d9f90b | |||
| 4120d6451e | |||
| 96d685fdf2 | |||
| b87fead2fd | |||
| 8c7b9b45fd | |||
| e4b5841c93 | |||
| cead3e42b8 | |||
| bfaa792760 |
211
AGENTS.md
Normal file
211
AGENTS.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# AGENTS.md — VoicePaste
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
VoicePaste: phone as microphone via browser → LAN WebSocket → Go server → Doubao ASR → real-time preview on phone → auto-paste to computer's focused app. Single Go binary with embedded frontend.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: Go 1.25+, Fiber v3, fasthttp/websocket, CGO required (robotgo + clipboard)
|
||||||
|
- **Frontend**: React 19, TypeScript, Zustand, Vite 7, Tailwind CSS v4, Biome 2, bun (package manager + runtime)
|
||||||
|
- **Tooling**: Taskfile (not Make), mise (Go + bun + task)
|
||||||
|
- **ASR**: Doubao Seed-ASR-2.0 via custom binary WebSocket protocol
|
||||||
|
|
||||||
|
## Build & Run Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install mise tools (go, bun, task)
|
||||||
|
mise install
|
||||||
|
|
||||||
|
# Build everything (frontend + Go binary → dist/)
|
||||||
|
task
|
||||||
|
|
||||||
|
# Build frontend only
|
||||||
|
task build:frontend
|
||||||
|
|
||||||
|
# Run (build + execute)
|
||||||
|
task run
|
||||||
|
|
||||||
|
# Dev mode (go run, skips frontend build)
|
||||||
|
task dev
|
||||||
|
|
||||||
|
# Clean all artifacts
|
||||||
|
task clean
|
||||||
|
|
||||||
|
# Tidy Go modules
|
||||||
|
task tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (run from `web/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install # Install deps
|
||||||
|
bun run build # Vite production build
|
||||||
|
bun run dev # Vite dev server
|
||||||
|
bun run lint # Biome check (lint + format)
|
||||||
|
bun run lint:fix # Biome check --write (auto-fix)
|
||||||
|
bun run typecheck # tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go vet ./... # Lint
|
||||||
|
go build -o dist/voicepaste . # Build (add .exe on Windows)
|
||||||
|
```
|
||||||
|
|
||||||
|
No test suite exists yet. No `go test` targets.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go # Entry point, embed.FS, TLS init, server startup
|
||||||
|
internal/
|
||||||
|
config/config.go # YAML + env var config, fsnotify hot-reload, atomic global
|
||||||
|
server/server.go # Fiber v3 HTTPS server, static files from embed.FS
|
||||||
|
server/net.go # LAN IP detection
|
||||||
|
tls/tls.go # AnyIP cert download/cache + self-signed fallback
|
||||||
|
tls/generate.go # Self-signed cert generation
|
||||||
|
ws/protocol.go # JSON message types (start/stop/paste/partial/final/pasted/error)
|
||||||
|
ws/handler.go # WS upgrade, token auth, session lifecycle, text accumulation, paste
|
||||||
|
asr/protocol.go # Doubao binary protocol codec (4-byte header, gzip)
|
||||||
|
asr/client.go # WSS client to Doubao, audio streaming, result forwarding
|
||||||
|
paste/paste.go # clipboard.Write + robotgo key simulation (Ctrl+V / Cmd+V)
|
||||||
|
web/
|
||||||
|
index.html # HTML shell with React root
|
||||||
|
vite.config.ts # Vite config (React + Tailwind plugins)
|
||||||
|
biome.json # Biome config (lint, format, Tailwind class sorting)
|
||||||
|
tsconfig.json # TypeScript strict config (React JSX)
|
||||||
|
src/
|
||||||
|
main.tsx # React entry point
|
||||||
|
App.tsx # Root component: composes hooks + layout
|
||||||
|
app.css # Tailwind imports, design tokens (@theme), keyframes
|
||||||
|
stores/
|
||||||
|
app-store.ts # Zustand store: connection, recording, preview, history, toast
|
||||||
|
hooks/
|
||||||
|
useWebSocket.ts # WS client hook: connect, reconnect, message dispatch
|
||||||
|
useRecorder.ts # Audio pipeline hook: WebVoiceProcessor (16kHz Int16 PCM capture)
|
||||||
|
components/
|
||||||
|
StatusBadge.tsx # Connection status indicator
|
||||||
|
PreviewBox.tsx # Real-time transcription preview
|
||||||
|
MicButton.tsx # Push-to-talk button with animations
|
||||||
|
HistoryList.tsx # Transcription history with re-send
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style — Go
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
Group in stdlib → external → internal order, separated by blank lines:
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
||||||
|
"github.com/imbytecat/voicepaste/internal/config"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Use aliases only to avoid collisions: `crypto_tls "crypto/tls"`, `vpTLS "...internal/tls"`, `wsMsg "...internal/ws"`.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
Use `log/slog` exclusively. Structured key-value pairs:
|
||||||
|
```go
|
||||||
|
slog.Info("message", "key", value)
|
||||||
|
slog.Error("failed to X", "err", err)
|
||||||
|
```
|
||||||
|
Per-connection loggers via `slog.With("remote", addr)`.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Always wrap with context: `fmt.Errorf("dial doubao: %w", err)`
|
||||||
|
- Return errors up; log at the boundary (main, handler entry)
|
||||||
|
- Never suppress errors silently. `slog.Warn` for non-fatal, `slog.Error` + exit/return for fatal
|
||||||
|
- Never use `as any`, `@ts-ignore`, or empty catch blocks
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
- Package names: short, lowercase, single word (`asr`, `ws`, `paste`, `config`)
|
||||||
|
- Exported types: `PascalCase` with doc comments
|
||||||
|
- Unexported: `camelCase`
|
||||||
|
- Constants: `PascalCase` for exported, `camelCase` for unexported
|
||||||
|
- Acronyms stay uppercase: `ASR`, `TLS`, `WS`, `URL`, `IP`
|
||||||
|
|
||||||
|
### Patterns
|
||||||
|
- `sync.Mutex` for shared state, `chan` for goroutine communication
|
||||||
|
- `atomic.Value` for hot-reloadable config
|
||||||
|
- Goroutine cleanup: `defer`, `sync.WaitGroup`, `closeCh chan struct{}`
|
||||||
|
- Fiber v3 middleware pattern for auth checks before WS upgrade
|
||||||
|
|
||||||
|
## Code Style — TypeScript (Frontend)
|
||||||
|
|
||||||
|
### Formatting (Biome)
|
||||||
|
- Indent: tabs
|
||||||
|
- Quotes: double quotes
|
||||||
|
- Semicolons: default (enabled)
|
||||||
|
- Organize imports: enabled via Biome assist
|
||||||
|
|
||||||
|
### TypeScript Config
|
||||||
|
- `strict: true`, `noUnusedLocals`, `noUnusedParameters`
|
||||||
|
- Target: ES2022, module: ESNext, bundler resolution
|
||||||
|
- DOM + DOM.Iterable libs
|
||||||
|
|
||||||
|
- React 19 with functional components and hooks
|
||||||
|
- Zustand for global state management (connection, recording, preview, history, toast)
|
||||||
|
- Custom hooks for imperative APIs: `useWebSocket`, `useRecorder`
|
||||||
|
- Zustand `getState()` in hooks/callbacks to avoid stale closures
|
||||||
|
- Pointer Events for touch/mouse (not touch + mouse separately)
|
||||||
|
- @picovoice/web-voice-processor for audio capture (16kHz Int16 PCM, WASM resampling)
|
||||||
|
- WebVoiceProcessor handles getUserMedia, AudioContext lifecycle, cross-browser compat
|
||||||
|
- WebSocket: binary for audio frames, JSON text for control messages
|
||||||
|
- Tailwind CSS v4 with `@theme` design tokens; minimal custom CSS (keyframes only)
|
||||||
|
|
||||||
|
## Language & Locale
|
||||||
|
|
||||||
|
- **UI text**: Chinese (中文) — this app is for family members
|
||||||
|
- **Git commits**: Chinese, conventional format: `feat:`, `fix:`, `chore:`, `refactor:`
|
||||||
|
- **Code comments**: English
|
||||||
|
- **Communication with user**: Chinese (中文)
|
||||||
|
|
||||||
|
## Key Constraints
|
||||||
|
|
||||||
|
- CGO is required (robotgo, clipboard) — no cross-compilation
|
||||||
|
- Token auth: read from `config.yaml`; empty = no auth. Never auto-generate tokens
|
||||||
|
- Frontend is embedded via `//go:embed all:web/dist` in `main.go`
|
||||||
|
- `embed` directive cannot use `../` paths — must be in the package referencing it
|
||||||
|
- Build output goes to `dist/` (gitignored)
|
||||||
|
- Frontend ignores (`node_modules`, `dist`) in `web/.gitignore`, not root
|
||||||
|
- Config file (`config.yaml`) is gitignored; `config.example.yaml` is committed
|
||||||
|
- `os.UserCacheDir()` for platform-correct cert cache paths
|
||||||
|
- robotgo paste: `KeyDown(modifier)` → delay → `KeyTap("v")` → delay → `KeyUp(modifier)`
|
||||||
|
|
||||||
|
## Hotwords (热词) Feature
|
||||||
|
|
||||||
|
Local hotword management for improved ASR accuracy on specific terms (names, technical vocabulary).
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
doubao:
|
||||||
|
hotwords:
|
||||||
|
- 张三
|
||||||
|
- 李四
|
||||||
|
- VoicePaste
|
||||||
|
- 人工智能
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
- Hotwords stored locally in `config.yaml` (not tied to cloud provider)
|
||||||
|
- `BuildHotwordsContext()` converts string array to Doubao API format:
|
||||||
|
```json
|
||||||
|
{"hotwords":[{"word":"张三"},{"word":"李四"}]}
|
||||||
|
```
|
||||||
|
- Sent via `corpus.context` parameter in `FullClientRequest`
|
||||||
|
- Hot-reloadable: config changes apply to new connections
|
||||||
|
- Platform-agnostic design: easy to migrate to other ASR providers
|
||||||
|
|
||||||
|
### Doubao API Details
|
||||||
|
|
||||||
|
- Parameter: `request.corpus.context` (JSON string)
|
||||||
|
- Limits: 100 tokens (双向流式), 5000 tokens (流式输入)
|
||||||
|
- Priority: `context` hotwords > `boosting_table_id` (if both present)
|
||||||
|
- No weight support in `context` mode (unlike `boosting_table_id`)
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
# VoicePaste config
|
# VoicePaste config
|
||||||
# Environment variables override these values (prefix: none, direct mapping)
|
# Environment variables override these values (prefix: VOICEPASTE_)
|
||||||
|
|
||||||
# 火山引擎豆包 ASR 配置
|
# ASR 通用配置
|
||||||
|
asr:
|
||||||
|
provider: doubao # env: VOICEPASTE_ASR_PROVIDER — ASR 引擎(目前支持: doubao)
|
||||||
|
hotwords: # 可选:热词列表,提升特定词汇识别准确率
|
||||||
|
# - 张三
|
||||||
|
# - 李四
|
||||||
|
# - VoicePaste
|
||||||
|
# - 人工智能
|
||||||
|
|
||||||
|
# 火山引擎豆包 ASR 凭证
|
||||||
doubao:
|
doubao:
|
||||||
app_key: "" # env: DOUBAO_APP_KEY
|
app_id: "" # env: VOICEPASTE_DOUBAO_APP_ID
|
||||||
access_key: "" # env: DOUBAO_ACCESS_KEY
|
access_token: "" # env: VOICEPASTE_DOUBAO_ACCESS_TOKEN
|
||||||
resource_id: "volc.seedasr.sauc.duration" # env: DOUBAO_RESOURCE_ID
|
resource_id: "volc.seedasr.sauc.duration" # env: VOICEPASTE_DOUBAO_RESOURCE_ID
|
||||||
|
|
||||||
# 服务配置
|
# 服务配置
|
||||||
server:
|
server:
|
||||||
port: 8443 # env: PORT
|
port: 8443 # env: VOICEPASTE_SERVER_PORT
|
||||||
tls_auto: true # env: TLS_AUTO — 自动 TLS (AnyIP + 自签名降级)
|
token: "" # env: VOICEPASTE_SERVER_TOKEN — 留空则不需要认证;填写后访问需携带 token 参数
|
||||||
|
|
||||||
# 安全配置
|
|
||||||
security:
|
|
||||||
token: "" # 留空则不需要认证;填写后访问需携带 token 参数
|
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/gofiber/contrib/v3/websocket v1.0.0
|
github.com/gofiber/contrib/v3/websocket v1.0.0
|
||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
golang.design/x/clipboard v0.7.1
|
golang.design/x/clipboard v0.7.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
@@ -19,22 +20,29 @@ require (
|
|||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
github.com/gen2brain/shm v0.1.1 // indirect
|
github.com/gen2brain/shm v0.1.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/jezek/xgb v1.2.0 // indirect
|
github.com/jezek/xgb v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/otiai10/gosseract/v2 v2.4.1 // indirect
|
github.com/otiai10/gosseract/v2 v2.4.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/robotn/xgb v0.10.0 // indirect
|
github.com/robotn/xgb v0.10.0 // indirect
|
||||||
github.com/robotn/xgbutil v0.10.0 // indirect
|
github.com/robotn/xgbutil v0.10.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
|
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071 // indirect
|
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071 // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
@@ -46,7 +54,10 @@ require (
|
|||||||
github.com/vcaesar/keycode v0.10.1 // indirect
|
github.com/vcaesar/keycode v0.10.1 // indirect
|
||||||
github.com/vcaesar/screenshot v0.11.1 // indirect
|
github.com/vcaesar/screenshot v0.11.1 // indirect
|
||||||
github.com/vcaesar/tt v0.20.1 // indirect
|
github.com/vcaesar/tt v0.20.1 // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
||||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||||
|
|||||||
27
go.sum
27
go.sum
@@ -2,7 +2,6 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D
|
|||||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
||||||
@@ -11,6 +10,8 @@ github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s
|
|||||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
@@ -22,6 +23,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-vgo/robotgo v1.0.1 h1:4dS+dXSMPRt+VmvG4QZPlH9BNG9Jfywq4q0YjSiFN0A=
|
github.com/go-vgo/robotgo v1.0.1 h1:4dS+dXSMPRt+VmvG4QZPlH9BNG9Jfywq4q0YjSiFN0A=
|
||||||
github.com/go-vgo/robotgo v1.0.1/go.mod h1:NcSL/tqNqkpWJ3rmT6YSDUVhQKZwyRsaanDMO4qkT5I=
|
github.com/go-vgo/robotgo v1.0.1/go.mod h1:NcSL/tqNqkpWJ3rmT6YSDUVhQKZwyRsaanDMO4qkT5I=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/gofiber/contrib/v3/websocket v1.0.0 h1:HZNjtiq1HbfTxMOftrwuHtafmwPV8ia2WU2BX0MX7Gg=
|
github.com/gofiber/contrib/v3/websocket v1.0.0 h1:HZNjtiq1HbfTxMOftrwuHtafmwPV8ia2WU2BX0MX7Gg=
|
||||||
@@ -56,6 +59,8 @@ github.com/otiai10/gosseract/v2 v2.4.1 h1:G8AyBpXEeSlcq8TI85LH/pM5SXk8Djy2GEXisg
|
|||||||
github.com/otiai10/gosseract/v2 v2.4.1/go.mod h1:1gNWP4Hgr2o7yqWfs6r5bZxAatjOIdqWxJLWsTsembk=
|
github.com/otiai10/gosseract/v2 v2.4.1/go.mod h1:1gNWP4Hgr2o7yqWfs6r5bZxAatjOIdqWxJLWsTsembk=
|
||||||
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
@@ -69,14 +74,28 @@ github.com/robotn/xgbutil v0.10.0 h1:gvf7mGQqCWQ68aHRtCxgdewRk+/KAJui6l3MJQQRCKw
|
|||||||
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
|
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
|
||||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
|
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
|
||||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||||
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
|
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
|
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071 h1:qo7kOhoN5DHioXNlFytBzIoA5glW6lsb8YqV0lP3IyE=
|
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071 h1:qo7kOhoN5DHioXNlFytBzIoA5glW6lsb8YqV0lP3IyE=
|
||||||
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
|
github.com/tailscale/win v0.0.0-20250627215312-f4da2b8ee071/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||||
@@ -101,12 +120,18 @@ github.com/vcaesar/screenshot v0.11.1 h1:GgPuN89XC4Yh38dLx4quPlSo3YiWWhwIria/j3L
|
|||||||
github.com/vcaesar/screenshot v0.11.1/go.mod h1:gJNwHBiP1v1v7i8TQ4yV1XJtcyn2I/OJL7OziVQkwjs=
|
github.com/vcaesar/screenshot v0.11.1/go.mod h1:gJNwHBiP1v1v7i8TQ4yV1XJtcyn2I/OJL7OziVQkwjs=
|
||||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||||
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
||||||
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
|||||||
@@ -13,16 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
doubaoEndpoint = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream"
|
doubaoEndpoint = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
|
||||||
writeTimeout = 10 * time.Second
|
writeTimeout = 10 * time.Second
|
||||||
readTimeout = 30 * time.Second
|
readTimeout = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds Doubao ASR connection parameters.
|
// Config holds Doubao ASR connection parameters.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppKey string
|
AppID string
|
||||||
AccessKey string
|
AccessToken string
|
||||||
ResourceID string
|
ResourceID string
|
||||||
|
Hotwords []string // 本地热词列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client manages a single ASR session with Doubao.
|
// Client manages a single ASR session with Doubao.
|
||||||
@@ -39,8 +40,8 @@ type Client struct {
|
|||||||
func Dial(cfg Config, resultCh chan<- wsMsg.ServerMsg) (*Client, error) {
|
func Dial(cfg Config, resultCh chan<- wsMsg.ServerMsg) (*Client, error) {
|
||||||
connID := uuid.New().String()
|
connID := uuid.New().String()
|
||||||
headers := http.Header{
|
headers := http.Header{
|
||||||
"X-Api-App-Key": {cfg.AppKey},
|
"X-Api-App-Key": {cfg.AppID},
|
||||||
"X-Api-Access-Key": {cfg.AccessKey},
|
"X-Api-Access-Key": {cfg.AccessToken},
|
||||||
"X-Api-Resource-Id": {cfg.ResourceID},
|
"X-Api-Resource-Id": {cfg.ResourceID},
|
||||||
"X-Api-Connect-Id": {connID},
|
"X-Api-Connect-Id": {connID},
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,17 @@ func Dial(cfg Config, resultCh chan<- wsMsg.ServerMsg) (*Client, error) {
|
|||||||
closeCh: make(chan struct{}),
|
closeCh: make(chan struct{}),
|
||||||
log: slog.With("conn_id", connID),
|
log: slog.With("conn_id", connID),
|
||||||
}
|
}
|
||||||
|
// Build corpus configuration
|
||||||
|
var corpus *Corpus
|
||||||
|
if len(cfg.Hotwords) > 0 {
|
||||||
|
contextJSON, err := BuildHotwordsContext(cfg.Hotwords)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("failed to build hotwords context, skipping", "err", err)
|
||||||
|
} else {
|
||||||
|
corpus = &Corpus{Context: contextJSON}
|
||||||
|
slog.Info("hotwords enabled", "count", len(cfg.Hotwords))
|
||||||
|
}
|
||||||
|
}
|
||||||
// Send FullClientRequest
|
// Send FullClientRequest
|
||||||
req := &FullClientRequest{
|
req := &FullClientRequest{
|
||||||
User: UserMeta{UID: connID},
|
User: UserMeta{UID: connID},
|
||||||
@@ -68,13 +80,15 @@ func Dial(cfg Config, resultCh chan<- wsMsg.ServerMsg) (*Client, error) {
|
|||||||
Channel: 1,
|
Channel: 1,
|
||||||
},
|
},
|
||||||
Request: RequestMeta{
|
Request: RequestMeta{
|
||||||
ModelName: "seedasr-2.0",
|
ModelName: "seedasr-2.0",
|
||||||
EnableITN: true,
|
EnableITN: true,
|
||||||
EnablePUNC: true,
|
EnablePUNC: true,
|
||||||
EnableDDC: true,
|
EnableDDC: true,
|
||||||
ShowUtterances: false,
|
ShowUtterances: true,
|
||||||
ResultType: "single",
|
ResultType: "full",
|
||||||
EndWindowSize: 400,
|
EnableNonstream: true,
|
||||||
|
EndWindowSize: 800,
|
||||||
|
Corpus: corpus,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
data, err := EncodeFullClientRequest(req)
|
data, err := EncodeFullClientRequest(req)
|
||||||
@@ -132,10 +146,15 @@ func (c *Client) readLoop(resultCh chan<- wsMsg.ServerMsg) {
|
|||||||
resultCh <- wsMsg.ServerMsg{Type: wsMsg.MsgError, Message: resp.ErrMsg}
|
resultCh <- wsMsg.ServerMsg{Type: wsMsg.MsgError, Message: resp.ErrMsg}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// nostream mode: result comes after last audio packet or >15s
|
// bigmodel_async with enable_nonstream: returns both streaming (partial) and definite (final) results
|
||||||
text := resp.Text
|
text := resp.Text
|
||||||
if text != "" {
|
if text != "" {
|
||||||
resultCh <- wsMsg.ServerMsg{Type: wsMsg.MsgFinal, Text: text}
|
if resp.IsLast {
|
||||||
|
resultCh <- wsMsg.ServerMsg{Type: wsMsg.MsgFinal, Text: text}
|
||||||
|
} else {
|
||||||
|
// Intermediate streaming result (first pass) — preview only
|
||||||
|
resultCh <- wsMsg.ServerMsg{Type: wsMsg.MsgPartial, Text: text}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if resp.IsLast {
|
if resp.IsLast {
|
||||||
return
|
return
|
||||||
|
|||||||
43
internal/asr/hotwords.go
Normal file
43
internal/asr/hotwords.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package asr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HotwordEntry represents a single hotword for context JSON.
|
||||||
|
type HotwordEntry struct {
|
||||||
|
Word string `json:"word"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HotwordsContext represents the context JSON structure for hotwords.
|
||||||
|
type HotwordsContext struct {
|
||||||
|
Hotwords []HotwordEntry `json:"hotwords"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildHotwordsContext converts a list of hotword strings to context JSON string.
|
||||||
|
// Returns empty string if hotwords list is empty.
|
||||||
|
func BuildHotwordsContext(hotwords []string) (string, error) {
|
||||||
|
if len(hotwords) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]HotwordEntry, 0, len(hotwords))
|
||||||
|
for _, word := range hotwords {
|
||||||
|
if word != "" {
|
||||||
|
entries = append(entries, HotwordEntry{Word: word})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := HotwordsContext{Hotwords: entries}
|
||||||
|
data, err := json.Marshal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("marshal hotwords context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
@@ -103,14 +103,21 @@ type AudioMeta struct {
|
|||||||
Channel int `json:"channel"`
|
Channel int `json:"channel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Corpus holds hotwords and context configuration.
|
||||||
|
type Corpus struct {
|
||||||
|
Context string `json:"context,omitempty"` // 热词直传 JSON
|
||||||
|
}
|
||||||
|
|
||||||
type RequestMeta struct {
|
type RequestMeta struct {
|
||||||
ModelName string `json:"model_name"`
|
ModelName string `json:"model_name"`
|
||||||
EnableITN bool `json:"enable_itn"`
|
EnableITN bool `json:"enable_itn"`
|
||||||
EnablePUNC bool `json:"enable_punc"`
|
EnablePUNC bool `json:"enable_punc"`
|
||||||
EnableDDC bool `json:"enable_ddc"`
|
EnableDDC bool `json:"enable_ddc"`
|
||||||
ShowUtterances bool `json:"show_utterances"`
|
ShowUtterances bool `json:"show_utterances"`
|
||||||
ResultType string `json:"result_type,omitempty"`
|
ResultType string `json:"result_type,omitempty"`
|
||||||
EndWindowSize int `json:"end_window_size,omitempty"`
|
EnableNonstream bool `json:"enable_nonstream,omitempty"`
|
||||||
|
EndWindowSize int `json:"end_window_size,omitempty"`
|
||||||
|
Corpus *Corpus `json:"corpus,omitempty"` // 语料/热词配置
|
||||||
}
|
}
|
||||||
// EncodeFullClientRequest builds the binary message for the initial handshake.
|
// EncodeFullClientRequest builds the binary message for the initial handshake.
|
||||||
// nostream mode: header(4) + payload_size(4) + gzip(json)
|
// nostream mode: header(4) + payload_size(4) + gzip(json)
|
||||||
|
|||||||
@@ -1,59 +1,279 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ASRConfig holds ASR settings independent of any specific provider.
|
||||||
|
type ASRConfig struct {
|
||||||
|
Provider string `mapstructure:"provider"`
|
||||||
|
Hotwords []string `mapstructure:"hotwords"` // 通用热词列表
|
||||||
|
}
|
||||||
|
|
||||||
// DoubaoConfig holds 火山引擎豆包 ASR credentials.
|
// DoubaoConfig holds 火山引擎豆包 ASR credentials.
|
||||||
type DoubaoConfig struct {
|
type DoubaoConfig struct {
|
||||||
AppKey string `yaml:"app_key"`
|
AppID string `mapstructure:"app_id"`
|
||||||
AccessKey string `yaml:"access_key"`
|
AccessToken string `mapstructure:"access_token"`
|
||||||
ResourceID string `yaml:"resource_id"`
|
ResourceID string `mapstructure:"resource_id"`
|
||||||
}
|
|
||||||
|
|
||||||
// SecurityConfig holds authentication settings.
|
|
||||||
type SecurityConfig struct {
|
|
||||||
Token string `yaml:"token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig holds server settings.
|
// ServerConfig holds server settings.
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `yaml:"port"`
|
Port int `mapstructure:"port"`
|
||||||
TLSAuto bool `yaml:"tls_auto"`
|
Token string `mapstructure:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is the top-level configuration.
|
// Config is the top-level configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Doubao DoubaoConfig `yaml:"doubao"`
|
ASR ASRConfig `mapstructure:"asr"`
|
||||||
Server ServerConfig `yaml:"server"`
|
Doubao DoubaoConfig `mapstructure:"doubao"`
|
||||||
Security SecurityConfig `yaml:"security"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaults returns a Config with default values.
|
// defaults returns a Config with default values.
|
||||||
func defaults() Config {
|
func defaults() Config {
|
||||||
return Config{
|
return Config{
|
||||||
|
ASR: ASRConfig{
|
||||||
|
Provider: "doubao",
|
||||||
|
},
|
||||||
Doubao: DoubaoConfig{
|
Doubao: DoubaoConfig{
|
||||||
ResourceID: "volc.seedasr.sauc.duration",
|
ResourceID: "volc.seedasr.sauc.duration",
|
||||||
},
|
},
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 8443,
|
Port: 8443,
|
||||||
TLSAuto: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// global holds the current config atomically for concurrent reads.
|
// global holds the current config atomically for concurrent reads.
|
||||||
var global atomic.Value
|
var global atomic.Value
|
||||||
|
var watcher *fsnotify.Watcher
|
||||||
|
var watcherMu sync.Mutex
|
||||||
|
var watchStarted bool
|
||||||
|
var watchStartErr error
|
||||||
|
|
||||||
|
// Load reads config from file (or uses defaults if file doesn't exist).
|
||||||
|
// Empty path defaults to "config.yaml".
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
if path == "" {
|
||||||
|
path = "config.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigFile(path)
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
def := defaults()
|
||||||
|
v.SetDefault("asr.provider", def.ASR.Provider)
|
||||||
|
v.SetDefault("doubao.resource_id", def.Doubao.ResourceID)
|
||||||
|
v.SetDefault("server.port", def.Server.Port)
|
||||||
|
|
||||||
|
// Allow env var overrides (e.g., VOICEPASTE_DOUBAO_APP_ID)
|
||||||
|
v.SetEnvPrefix("voicepaste")
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
v.AutomaticEnv()
|
||||||
|
|
||||||
|
// Read config file (ignore error if file doesn't exist)
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
slog.Warn("config file not found, using defaults", "path", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
// Validate before storing
|
||||||
|
if err := validate(cfg); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
store(cfg)
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate checks required fields based on the configured ASR provider.
|
||||||
|
func validate(cfg Config) error {
|
||||||
|
provider := strings.TrimSpace(strings.ToLower(cfg.ASR.Provider))
|
||||||
|
switch provider {
|
||||||
|
case "doubao":
|
||||||
|
if cfg.Doubao.AppID == "" {
|
||||||
|
return fmt.Errorf("doubao.app_id is required when asr.provider is \"doubao\"")
|
||||||
|
}
|
||||||
|
if cfg.Doubao.AccessToken == "" {
|
||||||
|
return fmt.Errorf("doubao.access_token is required when asr.provider is \"doubao\"")
|
||||||
|
}
|
||||||
|
if cfg.Doubao.ResourceID == "" {
|
||||||
|
return fmt.Errorf("doubao.resource_id is required when asr.provider is \"doubao\"")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported asr.provider: %q (supported: doubao)", cfg.ASR.Provider)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchAndReload starts watching config file for changes and reloads automatically.
|
||||||
|
// Empty path defaults to "config.yaml".
|
||||||
|
// Returns a function to stop watching. Can only be called once.
|
||||||
|
func WatchAndReload(path string) func() {
|
||||||
|
watcherMu.Lock()
|
||||||
|
defer watcherMu.Unlock()
|
||||||
|
// Check if already started
|
||||||
|
if watchStarted {
|
||||||
|
if watchStartErr != nil {
|
||||||
|
return func() {} // Previous start failed, return no-op
|
||||||
|
}
|
||||||
|
// Already running, return existing stop function
|
||||||
|
return func() {
|
||||||
|
watcherMu.Lock()
|
||||||
|
defer watcherMu.Unlock()
|
||||||
|
if watcher != nil {
|
||||||
|
if err := watcher.Close(); err != nil {
|
||||||
|
slog.Warn("failed to close config watcher", "err", err)
|
||||||
|
}
|
||||||
|
watcher = nil
|
||||||
|
watchStarted = false
|
||||||
|
slog.Info("config watcher stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = "config.yaml"
|
||||||
|
}
|
||||||
|
// Get absolute path for reliable matching
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to resolve config path", "err", err)
|
||||||
|
watchStartErr = err
|
||||||
|
// Don't set watchStarted = true, allow retry
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
w, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to create config watcher", "err", err)
|
||||||
|
watchStartErr = err
|
||||||
|
// Don't set watchStarted = true, allow retry
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
watchDir := filepath.Dir(absPath)
|
||||||
|
if err := w.Add(watchDir); err != nil {
|
||||||
|
slog.Error("failed to watch config directory", "err", err, "dir", watchDir)
|
||||||
|
w.Close()
|
||||||
|
watchStartErr = err
|
||||||
|
// Don't set watchStarted = true, allow retry
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
// Assign to global watcher before marking as started
|
||||||
|
watcher = w
|
||||||
|
watchStarted = true
|
||||||
|
watchStartErr = nil
|
||||||
|
// Create Viper instance for reading config
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigFile(absPath) // Use absolute path for consistency
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
// Set defaults
|
||||||
|
def := defaults()
|
||||||
|
v.SetDefault("asr.provider", def.ASR.Provider)
|
||||||
|
v.SetDefault("doubao.resource_id", def.Doubao.ResourceID)
|
||||||
|
v.SetDefault("server.port", def.Server.Port)
|
||||||
|
v.SetEnvPrefix("voicepaste")
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
v.AutomaticEnv()
|
||||||
|
// Initial read (ignore error if file doesn't exist)
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
|
slog.Warn("config watch: initial read failed", "err", err)
|
||||||
|
} else {
|
||||||
|
slog.Warn("config file not found, will watch for creation", "path", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Start event loop in goroutine
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-w.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Normalize event path for comparison
|
||||||
|
eventPath := filepath.Clean(event.Name)
|
||||||
|
if eventPath != absPath {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Process Write, Create, and Rename events (common editor patterns)
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Rename == fsnotify.Rename {
|
||||||
|
slog.Info("config file changed, reloading", "file", absPath, "op", event.Op)
|
||||||
|
// Re-read the file
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
slog.Error("config reload: read failed", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var cfg Config
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
slog.Error("config reload: unmarshal failed", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Validate before applying
|
||||||
|
if err := validate(cfg); err != nil {
|
||||||
|
slog.Warn("config reload: validation failed, keeping old config", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
store(cfg)
|
||||||
|
slog.Info("config reloaded and applied successfully")
|
||||||
|
}
|
||||||
|
case err, ok := <-w.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("config watcher error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Return stop function
|
||||||
|
return func() {
|
||||||
|
watcherMu.Lock()
|
||||||
|
defer watcherMu.Unlock()
|
||||||
|
if watcher != nil {
|
||||||
|
if err := watcher.Close(); err != nil {
|
||||||
|
slog.Warn("failed to close config watcher", "err", err)
|
||||||
|
}
|
||||||
|
watcher = nil
|
||||||
|
watchStarted = false
|
||||||
|
slog.Info("config watcher stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns the current config snapshot. Safe for concurrent use.
|
// Get returns the current config snapshot. Safe for concurrent use.
|
||||||
|
// Returns a deep copy to prevent external modifications.
|
||||||
func Get() Config {
|
func Get() Config {
|
||||||
if v := global.Load(); v != nil {
|
if v := global.Load(); v != nil {
|
||||||
return v.(Config)
|
cfg := v.(Config)
|
||||||
|
// Deep copy hotwords slice to prevent external modifications
|
||||||
|
if cfg.ASR.Hotwords != nil {
|
||||||
|
cfg.ASR.Hotwords = append([]string(nil), cfg.ASR.Hotwords...)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
}
|
}
|
||||||
return defaults()
|
return defaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// store updates the global config.
|
// store updates the global config.
|
||||||
|
// Deep copies slices to ensure immutability.
|
||||||
func store(cfg Config) {
|
func store(cfg Config) {
|
||||||
|
// Deep copy hotwords to prevent external modifications
|
||||||
|
if cfg.ASR.Hotwords != nil {
|
||||||
|
cfg.ASR.Hotwords = append([]string(nil), cfg.ASR.Hotwords...)
|
||||||
|
}
|
||||||
global.Store(cfg)
|
global.Store(cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Load reads config from file (optional), applies env overrides, validates, and stores globally.
|
|
||||||
// If configPath is empty, it tries "config.yaml" in the working directory.
|
|
||||||
func Load(configPath string) (Config, error) {
|
|
||||||
cfg := defaults()
|
|
||||||
|
|
||||||
// Try loading YAML file
|
|
||||||
if configPath == "" {
|
|
||||||
configPath = "config.yaml"
|
|
||||||
}
|
|
||||||
if data, err := os.ReadFile(configPath); err == nil {
|
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return cfg, fmt.Errorf("parse config %s: %w", configPath, err)
|
|
||||||
}
|
|
||||||
slog.Info("loaded config file", "path", configPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Env overrides
|
|
||||||
applyEnv(&cfg)
|
|
||||||
|
|
||||||
// Validate
|
|
||||||
if err := validate(cfg); err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
store(cfg)
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyEnv overrides config fields with environment variables.
|
|
||||||
func applyEnv(cfg *Config) {
|
|
||||||
if v := os.Getenv("DOUBAO_APP_KEY"); v != "" {
|
|
||||||
cfg.Doubao.AppKey = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("DOUBAO_ACCESS_KEY"); v != "" {
|
|
||||||
cfg.Doubao.AccessKey = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("DOUBAO_RESOURCE_ID"); v != "" {
|
|
||||||
cfg.Doubao.ResourceID = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("PORT"); v != "" {
|
|
||||||
if port, err := strconv.Atoi(v); err == nil {
|
|
||||||
cfg.Server.Port = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v := os.Getenv("TLS_AUTO"); v != "" {
|
|
||||||
cfg.Server.TLSAuto = v == "true" || v == "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate checks required fields.
|
|
||||||
func validate(cfg Config) error {
|
|
||||||
if cfg.Doubao.AppKey == "" {
|
|
||||||
return fmt.Errorf("doubao.app_key is required (set DOUBAO_APP_KEY or config.yaml)")
|
|
||||||
}
|
|
||||||
if cfg.Doubao.AccessKey == "" {
|
|
||||||
return fmt.Errorf("doubao.access_key is required (set DOUBAO_ACCESS_KEY or config.yaml)")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchAndReload watches the config file for changes and hot-reloads.
|
|
||||||
func WatchAndReload(configPath string) {
|
|
||||||
if configPath == "" {
|
|
||||||
configPath = "config.yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
absPath, err := filepath.Abs(configPath)
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("cannot resolve config path for watching", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("cannot create file watcher", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := filepath.Dir(absPath)
|
|
||||||
if err := watcher.Add(dir); err != nil {
|
|
||||||
slog.Warn("cannot watch config directory", "error", err)
|
|
||||||
watcher.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("watching config for changes", "path", absPath)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer watcher.Close()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if filepath.Clean(event.Name) == absPath && (event.Has(fsnotify.Write) || event.Has(fsnotify.Create)) {
|
|
||||||
slog.Info("config file changed, reloading")
|
|
||||||
if _, err := Load(configPath); err != nil {
|
|
||||||
slog.Error("failed to reload config", "error", err)
|
|
||||||
} else {
|
|
||||||
slog.Info("config reloaded successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slog.Error("config watcher error", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// generateSelfSigned creates a self-signed certificate for the given IP,
|
|
||||||
// saves it to disk, and returns the tls.Certificate.
|
|
||||||
func generateSelfSigned(lanIP, certFile, keyFile string) (tls.Certificate, error) {
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("generate key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("generate serial: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"VoicePaste"},
|
|
||||||
CommonName: "VoicePaste Local",
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 year
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP(lanIP)},
|
|
||||||
DNSNames: []string{"localhost"},
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("create certificate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save cert PEM
|
|
||||||
certOut, err := os.Create(certFile)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("create cert file: %w", err)
|
|
||||||
}
|
|
||||||
defer certOut.Close()
|
|
||||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
||||||
|
|
||||||
// Save key PEM
|
|
||||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("marshal key: %w", err)
|
|
||||||
}
|
|
||||||
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, fmt.Errorf("create key file: %w", err)
|
|
||||||
}
|
|
||||||
defer keyOut.Close()
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
|
||||||
|
|
||||||
return tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -26,11 +25,10 @@ func certDir() string {
|
|||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result holds the TLS config and metadata about which cert source was used.
|
// Result holds the TLS config and the AnyIP hostname.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Config *tls.Config
|
Config *tls.Config
|
||||||
AnyIP bool // true if AnyIP cert is active
|
Host string // AnyIP hostname (e.g. voicepaste-192-168-1-5.anyip.dev)
|
||||||
Host string // hostname to use in URLs (AnyIP domain or raw IP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnyIPHost returns the AnyIP hostname for a given LAN IP.
|
// AnyIPHost returns the AnyIP hostname for a given LAN IP.
|
||||||
@@ -40,82 +38,61 @@ func AnyIPHost(lanIP string) string {
|
|||||||
return fmt.Sprintf("voicepaste-%s.anyip.dev", dashed)
|
return fmt.Sprintf("voicepaste-%s.anyip.dev", dashed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTLSConfig returns a TLS config for the given LAN IP.
|
// GetTLSConfig returns a TLS config using AnyIP wildcard certificate.
|
||||||
// Priority: cached AnyIP → download AnyIP → cached self-signed → generate self-signed.
|
// It tries cached cert first, then downloads fresh if needed.
|
||||||
func GetTLSConfig(lanIP string) (*Result, error) {
|
func GetTLSConfig(lanIP string) (*Result, error) {
|
||||||
dir := certDir()
|
dir := certDir()
|
||||||
anyipDir := filepath.Join(dir, "anyip")
|
anyipDir := filepath.Join(dir, "anyip")
|
||||||
os.MkdirAll(anyipDir, 0700)
|
os.MkdirAll(anyipDir, 0700)
|
||||||
anyipCert := filepath.Join(anyipDir, "fullchain.pem")
|
certFile := filepath.Join(anyipDir, "fullchain.pem")
|
||||||
anyipKey := filepath.Join(anyipDir, "privkey.pem")
|
keyFile := filepath.Join(anyipDir, "privkey.pem")
|
||||||
|
host := AnyIPHost(lanIP)
|
||||||
|
|
||||||
// 1. Try cached AnyIP cert
|
// Try cached cert first
|
||||||
if cert, err := tls.LoadX509KeyPair(anyipCert, anyipKey); err == nil {
|
if cert, err := loadAndValidateCert(certFile, keyFile); err == nil {
|
||||||
if leaf, err := x509.ParseCertificate(cert.Certificate[0]); err == nil {
|
slog.Info("using cached AnyIP certificate")
|
||||||
if time.Now().Before(leaf.NotAfter.Add(-24 * time.Hour)) { // 1 day buffer
|
return &Result{
|
||||||
slog.Info("using cached AnyIP certificate", "expires", leaf.NotAfter.Format("2006-01-02"))
|
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||||
return &Result{
|
Host: host,
|
||||||
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
}, nil
|
||||||
AnyIP: true,
|
|
||||||
Host: AnyIPHost(lanIP),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Try downloading AnyIP cert
|
// Download fresh cert
|
||||||
if err := downloadAnyIPCert(anyipCert, anyipKey); err == nil {
|
slog.Info("downloading AnyIP certificate")
|
||||||
if cert, err := tls.LoadX509KeyPair(anyipCert, anyipKey); err == nil {
|
if err := downloadAnyIPCert(certFile, keyFile); err != nil {
|
||||||
slog.Info("downloaded fresh AnyIP certificate")
|
return nil, fmt.Errorf("failed to download AnyIP certificate: %w", err)
|
||||||
return &Result{
|
|
||||||
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
|
||||||
AnyIP: true,
|
|
||||||
Host: AnyIPHost(lanIP),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slog.Warn("AnyIP cert download failed, falling back to self-signed", "err", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Try cached self-signed
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
ssCert := filepath.Join(dir, "cert.pem")
|
|
||||||
ssKey := filepath.Join(dir, "key.pem")
|
|
||||||
if cert, err := tls.LoadX509KeyPair(ssCert, ssKey); err == nil {
|
|
||||||
if leaf, err := x509.ParseCertificate(cert.Certificate[0]); err == nil {
|
|
||||||
if time.Now().Before(leaf.NotAfter) && certCoversIP(leaf, lanIP) {
|
|
||||||
slog.Info("using cached self-signed certificate", "expires", leaf.NotAfter.Format("2006-01-02"))
|
|
||||||
return &Result{
|
|
||||||
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
|
||||||
Host: lanIP,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Generate self-signed
|
|
||||||
slog.Info("generating self-signed TLS certificate", "ip", lanIP)
|
|
||||||
cert, err := generateSelfSigned(lanIP, ssCert, ssKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generate TLS cert: %w", err)
|
return nil, fmt.Errorf("failed to load downloaded certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Info("downloaded fresh AnyIP certificate")
|
||||||
return &Result{
|
return &Result{
|
||||||
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||||
Host: lanIP,
|
Host: host,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// certCoversIP checks if the certificate covers the given IP.
|
// loadAndValidateCert loads a certificate and validates it's not expired.
|
||||||
func certCoversIP(cert *x509.Certificate, ip string) bool {
|
func loadAndValidateCert(certFile, keyFile string) (tls.Certificate, error) {
|
||||||
target := net.ParseIP(ip)
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
if target == nil {
|
if err != nil {
|
||||||
return false
|
return tls.Certificate{}, err
|
||||||
}
|
}
|
||||||
for _, certIP := range cert.IPAddresses {
|
|
||||||
if certIP.Equal(target) {
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
return true
|
if err != nil {
|
||||||
}
|
return tls.Certificate{}, err
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
// Check if cert expires within 24 hours
|
||||||
|
if time.Now().After(leaf.NotAfter.Add(-24 * time.Hour)) {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("certificate expired or expiring soon")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadAnyIPCert downloads the AnyIP wildcard cert and key.
|
// downloadAnyIPCert downloads the AnyIP wildcard cert and key.
|
||||||
|
|||||||
@@ -2,29 +2,46 @@ package ws
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/contrib/v3/websocket"
|
"github.com/gofiber/contrib/v3/websocket"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PasteFunc is called when the server should paste text into the focused app.
|
const wsReadTimeout = 75 * time.Second
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
log *slog.Logger
|
||||||
|
resultCh chan ServerMsg
|
||||||
|
|
||||||
|
stateMu sync.Mutex
|
||||||
|
writeMu sync.Mutex
|
||||||
|
previewMu sync.Mutex
|
||||||
|
state string
|
||||||
|
sessionID string
|
||||||
|
seq int64
|
||||||
|
lastAudioSeq uint64
|
||||||
|
previewText string
|
||||||
|
sendAudio func([]byte)
|
||||||
|
cleanup func()
|
||||||
|
}
|
||||||
|
|
||||||
type PasteFunc func(text string) error
|
type PasteFunc func(text string) error
|
||||||
|
|
||||||
// ASRFactory creates an ASR session. It returns a channel that receives
|
|
||||||
// partial/final results, and a function to send audio frames.
|
|
||||||
// The cleanup function must be called when the session ends.
|
|
||||||
type ASRFactory func(resultCh chan<- ServerMsg) (sendAudio func(pcm []byte), cleanup func(), err error)
|
type ASRFactory func(resultCh chan<- ServerMsg) (sendAudio func(pcm []byte), cleanup func(), err error)
|
||||||
|
|
||||||
// Handler holds dependencies for WebSocket connections.
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
token string
|
token string
|
||||||
pasteFunc PasteFunc
|
pasteFunc PasteFunc
|
||||||
asrFactory ASRFactory
|
asrFactory ASRFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a WS handler with the given dependencies.
|
|
||||||
func NewHandler(token string, pasteFn PasteFunc, asrFn ASRFactory) *Handler {
|
func NewHandler(token string, pasteFn PasteFunc, asrFn ASRFactory) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
token: token,
|
token: token,
|
||||||
@@ -33,128 +50,353 @@ func NewHandler(token string, pasteFn PasteFunc, asrFn ASRFactory) *Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register adds the /ws route to the Fiber app.
|
|
||||||
func (h *Handler) Register(app *fiber.App) {
|
func (h *Handler) Register(app *fiber.App) {
|
||||||
// Token check middleware (before upgrade)
|
app.Use("/ws", func(c fiber.Ctx) error {
|
||||||
app.Use("/ws", func(c fiber.Ctx) error {
|
if !websocket.IsWebSocketUpgrade(c) {
|
||||||
if !websocket.IsWebSocketUpgrade(c) {
|
return fiber.ErrUpgradeRequired
|
||||||
return fiber.ErrUpgradeRequired
|
}
|
||||||
}
|
if h.token != "" {
|
||||||
if h.token != "" {
|
q := c.Query("token")
|
||||||
q := c.Query("token")
|
if q != h.token {
|
||||||
if q != h.token {
|
return c.Status(fiber.StatusUnauthorized).SendString("invalid token")
|
||||||
return c.Status(fiber.StatusUnauthorized).SendString("invalid token")
|
}
|
||||||
}
|
}
|
||||||
}
|
return c.Next()
|
||||||
return c.Next()
|
})
|
||||||
})
|
|
||||||
|
|
||||||
app.Get("/ws", websocket.New(h.handleConn))
|
app.Get("/ws", websocket.New(h.handleConn))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleConn(c *websocket.Conn) {
|
func (h *Handler) handleConn(c *websocket.Conn) {
|
||||||
log := slog.With("remote", c.RemoteAddr().String())
|
sess := &session{
|
||||||
log.Info("ws connected")
|
conn: c,
|
||||||
defer log.Info("ws disconnected")
|
log: slog.With("remote", c.RemoteAddr().String()),
|
||||||
|
resultCh: make(chan ServerMsg, 64),
|
||||||
|
state: StateIdle,
|
||||||
|
sessionID: "",
|
||||||
|
seq: 1,
|
||||||
|
lastAudioSeq: 0,
|
||||||
|
}
|
||||||
|
|
||||||
// Result channel for ASR → phone
|
sess.log.Info("ws connected")
|
||||||
resultCh := make(chan ServerMsg, 32)
|
defer sess.log.Info("ws disconnected")
|
||||||
defer close(resultCh)
|
defer close(sess.resultCh)
|
||||||
|
defer sess.forceStop()
|
||||||
|
|
||||||
// Writer goroutine: single writer to avoid concurrent writes
|
var wg sync.WaitGroup
|
||||||
var wg sync.WaitGroup
|
wg.Add(1)
|
||||||
wg.Add(1)
|
go sess.writerLoop(&wg)
|
||||||
go func() {
|
defer wg.Wait()
|
||||||
defer wg.Done()
|
|
||||||
for msg := range resultCh {
|
|
||||||
if err := c.WriteMessage(websocket.TextMessage, msg.Bytes()); err != nil {
|
|
||||||
log.Warn("ws write error", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Auto-paste on final result
|
|
||||||
if msg.Type == MsgFinal && msg.Text != "" && h.pasteFunc != nil {
|
|
||||||
if err := h.pasteFunc(msg.Text); err != nil {
|
|
||||||
log.Error("auto-paste failed", "err", err)
|
|
||||||
} else {
|
|
||||||
_ = c.WriteMessage(websocket.TextMessage, ServerMsg{Type: MsgPasted}.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ASR session state
|
_ = sess.writeJSON(ServerMsg{Type: MsgReady, Message: "ok"})
|
||||||
var (
|
_ = sess.writeJSON(ServerMsg{Type: MsgState, State: StateIdle})
|
||||||
sendAudio func([]byte)
|
|
||||||
cleanup func()
|
|
||||||
active bool
|
|
||||||
)
|
|
||||||
defer func() {
|
|
||||||
if cleanup != nil {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
mt, data, err := c.ReadMessage()
|
_ = c.SetReadDeadline(time.Now().Add(wsReadTimeout))
|
||||||
if err != nil {
|
mt, data, err := c.ReadMessage()
|
||||||
break
|
if err != nil {
|
||||||
}
|
break
|
||||||
|
}
|
||||||
|
switch mt {
|
||||||
|
case websocket.BinaryMessage:
|
||||||
|
sess.handleAudioFrame(data)
|
||||||
|
case websocket.TextMessage:
|
||||||
|
h.handleTextMessage(sess, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch mt {
|
func (s *session) writerLoop(wg *sync.WaitGroup) {
|
||||||
case websocket.BinaryMessage:
|
defer wg.Done()
|
||||||
// Audio frame
|
for msg := range s.resultCh {
|
||||||
if active && sendAudio != nil {
|
if msg.Type == MsgPartial || msg.Type == MsgFinal {
|
||||||
sendAudio(data)
|
s.previewMu.Lock()
|
||||||
}
|
s.previewText = msg.Text
|
||||||
|
s.previewMu.Unlock()
|
||||||
|
|
||||||
case websocket.TextMessage:
|
s.stateMu.Lock()
|
||||||
var msg ClientMsg
|
msg.SessionID = s.sessionID
|
||||||
if err := json.Unmarshal(data, &msg); err != nil {
|
msg.Seq = s.seq
|
||||||
log.Warn("invalid json", "err", err)
|
s.seq++
|
||||||
continue
|
s.stateMu.Unlock()
|
||||||
}
|
}
|
||||||
switch msg.Type {
|
|
||||||
case MsgStart:
|
|
||||||
if active {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sa, cl, err := h.asrFactory(resultCh)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("asr start failed", "err", err)
|
|
||||||
resultCh <- ServerMsg{Type: MsgError, Message: "ASR start failed"}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sendAudio = sa
|
|
||||||
cleanup = cl
|
|
||||||
active = true
|
|
||||||
log.Info("recording started")
|
|
||||||
|
|
||||||
case MsgStop:
|
if err := s.writeJSON(msg); err != nil {
|
||||||
if !active {
|
s.log.Warn("ws write error", "err", err)
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
if cleanup != nil {
|
}
|
||||||
cleanup()
|
}
|
||||||
cleanup = nil
|
|
||||||
}
|
|
||||||
sendAudio = nil
|
|
||||||
active = false
|
|
||||||
log.Info("recording stopped")
|
|
||||||
|
|
||||||
case MsgPaste:
|
func (s *session) writeJSON(msg ServerMsg) error {
|
||||||
if msg.Text == "" {
|
s.writeMu.Lock()
|
||||||
continue
|
defer s.writeMu.Unlock()
|
||||||
}
|
return s.conn.WriteMessage(websocket.TextMessage, msg.Bytes())
|
||||||
if h.pasteFunc != nil {
|
}
|
||||||
if err := h.pasteFunc(msg.Text); err != nil {
|
|
||||||
log.Error("paste failed", "err", err)
|
func (s *session) handleAudioFrame(data []byte) {
|
||||||
resultCh <- ServerMsg{Type: MsgError, Message: "paste failed"}
|
packet, err := decodeAudioPacket(data)
|
||||||
} else {
|
if err != nil {
|
||||||
resultCh <- ServerMsg{Type: MsgPasted}
|
s.log.Warn("invalid audio packet", "err", err)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
s.stateMu.Lock()
|
||||||
}
|
if s.state != StateRecording || s.sendAudio == nil {
|
||||||
}
|
s.stateMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if packet.SessionID != s.sessionID {
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if packet.Seq <= s.lastAudioSeq {
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSeq := s.lastAudioSeq
|
||||||
|
if lastSeq > 0 && packet.Seq > lastSeq+1 {
|
||||||
|
s.log.Warn(
|
||||||
|
"audio seq gap",
|
||||||
|
"session_id",
|
||||||
|
s.sessionID,
|
||||||
|
"last",
|
||||||
|
lastSeq,
|
||||||
|
"curr",
|
||||||
|
packet.Seq,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s.lastAudioSeq = packet.Seq
|
||||||
|
sendAudio := s.sendAudio
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
|
||||||
|
sendAudio(packet.PCM)
|
||||||
|
}
|
||||||
|
|
||||||
|
type audioPacket struct {
|
||||||
|
Version int `msgpack:"v"`
|
||||||
|
SessionID string `msgpack:"sessionId"`
|
||||||
|
Seq uint64 `msgpack:"seq"`
|
||||||
|
PCM []byte `msgpack:"pcm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAudioPacket(data []byte) (audioPacket, error) {
|
||||||
|
var packet audioPacket
|
||||||
|
if err := msgpack.Unmarshal(data, &packet); err != nil {
|
||||||
|
return audioPacket{}, fmt.Errorf("msgpack decode failed: %w", err)
|
||||||
|
}
|
||||||
|
if packet.Version != 1 {
|
||||||
|
return audioPacket{}, fmt.Errorf("unsupported version: %d", packet.Version)
|
||||||
|
}
|
||||||
|
if packet.SessionID == "" || len(packet.SessionID) > 96 {
|
||||||
|
return audioPacket{}, fmt.Errorf("invalid session id")
|
||||||
|
}
|
||||||
|
if packet.Seq == 0 {
|
||||||
|
return audioPacket{}, fmt.Errorf("invalid seq")
|
||||||
|
}
|
||||||
|
if len(packet.PCM) == 0 || len(packet.PCM)%2 != 0 {
|
||||||
|
return audioPacket{}, fmt.Errorf("invalid pcm size")
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleTextMessage(s *session, data []byte) {
|
||||||
|
var msg ClientMsg
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
s.log.Warn("invalid json", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case MsgHello:
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgReady, Message: "ok"})
|
||||||
|
s.stateMu.Lock()
|
||||||
|
state := s.state
|
||||||
|
sid := s.sessionID
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgState, State: state, SessionID: sid})
|
||||||
|
case MsgPing:
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgPong, TS: msg.TS})
|
||||||
|
case MsgStart:
|
||||||
|
h.handleStart(s, msg)
|
||||||
|
case MsgStop:
|
||||||
|
h.handleStop(s, msg)
|
||||||
|
case MsgPaste:
|
||||||
|
h.handlePaste(s, msg.Text, msg.SessionID)
|
||||||
|
default:
|
||||||
|
h.sendError(s, "bad_message", "unsupported message type", true, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleStart(s *session, msg ClientMsg) {
|
||||||
|
if msg.SessionID == "" {
|
||||||
|
msg.SessionID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if len(msg.SessionID) > 96 {
|
||||||
|
h.sendError(s, "invalid_session", "invalid sessionId", false, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.stateMu.Lock()
|
||||||
|
if s.state != StateIdle {
|
||||||
|
current := s.sessionID
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
h.sendError(s, "busy", "session is not idle", true, current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.state = StateRecording
|
||||||
|
s.sessionID = msg.SessionID
|
||||||
|
s.seq = 1
|
||||||
|
s.lastAudioSeq = 0
|
||||||
|
s.sendAudio = nil
|
||||||
|
s.cleanup = nil
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
|
||||||
|
s.previewMu.Lock()
|
||||||
|
s.previewText = ""
|
||||||
|
s.previewMu.Unlock()
|
||||||
|
|
||||||
|
sa, cl, err := h.asrFactory(s.resultCh)
|
||||||
|
if err != nil {
|
||||||
|
s.stateMu.Lock()
|
||||||
|
s.state = StateIdle
|
||||||
|
s.sessionID = ""
|
||||||
|
s.sendAudio = nil
|
||||||
|
s.cleanup = nil
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
h.sendError(s, "start_failed", "ASR start failed", true, "")
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgState, State: StateIdle})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.stateMu.Lock()
|
||||||
|
if s.sessionID != msg.SessionID || s.state != StateRecording {
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
cl()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.sendAudio = sa
|
||||||
|
s.cleanup = cl
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgStartAck, SessionID: msg.SessionID})
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgState, State: StateRecording, SessionID: msg.SessionID})
|
||||||
|
s.log.Info("recording started", "session_id", msg.SessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleStop(s *session, msg ClientMsg) {
|
||||||
|
s.stateMu.Lock()
|
||||||
|
if s.state == StateIdle {
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.state == StateStopping {
|
||||||
|
sid := s.sessionID
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgStopAck, SessionID: sid})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.SessionID != "" && msg.SessionID != s.sessionID {
|
||||||
|
current := s.sessionID
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
h.sendError(s, "session_mismatch", "stop sessionId mismatch", false, current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.state = StateStopping
|
||||||
|
sid := s.sessionID
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgStopAck, SessionID: sid})
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgState, State: StateStopping, SessionID: sid})
|
||||||
|
|
||||||
|
go h.finalizeStop(s, sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) finalizeStop(s *session, sid string) {
|
||||||
|
cleanup := s.detachCleanup(sid)
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.previewMu.Lock()
|
||||||
|
finalText := s.previewText
|
||||||
|
s.previewText = ""
|
||||||
|
s.previewMu.Unlock()
|
||||||
|
|
||||||
|
if finalText != "" && h.pasteFunc != nil {
|
||||||
|
if err := h.pasteFunc(finalText); err != nil {
|
||||||
|
h.sendError(s, "paste_failed", "auto-paste failed", true, sid)
|
||||||
|
} else {
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgPasted, SessionID: sid})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.stateMu.Lock()
|
||||||
|
if s.sessionID == sid {
|
||||||
|
s.state = StateIdle
|
||||||
|
s.sessionID = ""
|
||||||
|
s.seq = 1
|
||||||
|
s.lastAudioSeq = 0
|
||||||
|
s.sendAudio = nil
|
||||||
|
s.cleanup = nil
|
||||||
|
}
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgState, State: StateIdle})
|
||||||
|
s.log.Info("recording stopped", "session_id", sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handlePaste(s *session, text, sid string) {
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.pasteFunc != nil {
|
||||||
|
if err := h.pasteFunc(text); err != nil {
|
||||||
|
h.sendError(s, "paste_failed", "paste failed", true, sid)
|
||||||
|
} else {
|
||||||
|
_ = s.writeJSON(ServerMsg{Type: MsgPasted, SessionID: sid})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) sendError(s *session, code, message string, retryable bool, sid string) {
|
||||||
|
err := s.writeJSON(ServerMsg{
|
||||||
|
Type: MsgError,
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Retryable: retryable,
|
||||||
|
SessionID: sid,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("send error message failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) detachCleanup(sid string) func() {
|
||||||
|
s.stateMu.Lock()
|
||||||
|
defer s.stateMu.Unlock()
|
||||||
|
if sid != "" && s.sessionID != sid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cleanup := s.cleanup
|
||||||
|
s.cleanup = nil
|
||||||
|
s.sendAudio = nil
|
||||||
|
return cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) forceStop() {
|
||||||
|
cleanup := s.detachCleanup("")
|
||||||
|
if cleanup != nil {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
s.stateMu.Lock()
|
||||||
|
s.state = StateIdle
|
||||||
|
s.sessionID = ""
|
||||||
|
s.seq = 1
|
||||||
|
s.lastAudioSeq = 0
|
||||||
|
s.stateMu.Unlock()
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,31 +8,56 @@ import "encoding/json"
|
|||||||
type MsgType string
|
type MsgType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MsgStart MsgType = "start" // Begin recording session
|
MsgHello MsgType = "hello"
|
||||||
MsgStop MsgType = "stop" // End recording session
|
MsgStart MsgType = "start"
|
||||||
MsgPaste MsgType = "paste" // Re-paste a history item
|
MsgStop MsgType = "stop"
|
||||||
|
MsgPaste MsgType = "paste"
|
||||||
|
MsgPing MsgType = "ping"
|
||||||
|
MsgPong MsgType = "pong"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientMsg is a JSON control message from the phone.
|
// ClientMsg is a JSON control message from the phone.
|
||||||
type ClientMsg struct {
|
type ClientMsg struct {
|
||||||
Type MsgType `json:"type"`
|
Type MsgType `json:"type"`
|
||||||
Text string `json:"text,omitempty"` // Only for "paste"
|
SessionID string `json:"sessionId,omitempty"`
|
||||||
|
Seq int64 `json:"seq,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"` // Only for "paste"
|
||||||
|
Version int `json:"version,omitempty"`
|
||||||
|
TS int64 `json:"ts,omitempty"`
|
||||||
|
// Future extension: dynamic hotwords (Phase 2)
|
||||||
|
// Hotwords []string `json:"hotwords,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Server → Client messages ──
|
// ── Server → Client messages ──
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MsgPartial MsgType = "partial" // Interim ASR result
|
MsgReady MsgType = "ready"
|
||||||
MsgFinal MsgType = "final" // Final ASR result
|
MsgState MsgType = "state"
|
||||||
MsgPasted MsgType = "pasted" // Paste confirmed
|
MsgStartAck MsgType = "start_ack"
|
||||||
MsgError MsgType = "error" // Error notification
|
MsgStopAck MsgType = "stop_ack"
|
||||||
|
MsgPartial MsgType = "partial"
|
||||||
|
MsgFinal MsgType = "final"
|
||||||
|
MsgPasted MsgType = "pasted"
|
||||||
|
MsgError MsgType = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateIdle = "idle"
|
||||||
|
StateRecording = "recording"
|
||||||
|
StateStopping = "stopping"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerMsg is a JSON message sent to the phone.
|
// ServerMsg is a JSON message sent to the phone.
|
||||||
type ServerMsg struct {
|
type ServerMsg struct {
|
||||||
Type MsgType `json:"type"`
|
Type MsgType `json:"type"`
|
||||||
Text string `json:"text,omitempty"`
|
State string `json:"state,omitempty"`
|
||||||
Message string `json:"message,omitempty"` // For errors
|
SessionID string `json:"sessionId,omitempty"`
|
||||||
|
Seq int64 `json:"seq,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"` // For errors
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Retryable bool `json:"retryable,omitempty"`
|
||||||
|
TS int64 `json:"ts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ServerMsg) Bytes() []byte {
|
func (m ServerMsg) Bytes() []byte {
|
||||||
|
|||||||
166
main.go
166
main.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/imbytecat/voicepaste/internal/asr"
|
"github.com/imbytecat/voicepaste/internal/asr"
|
||||||
"github.com/imbytecat/voicepaste/internal/config"
|
"github.com/imbytecat/voicepaste/internal/config"
|
||||||
"github.com/imbytecat/voicepaste/internal/paste"
|
"github.com/imbytecat/voicepaste/internal/paste"
|
||||||
@@ -22,99 +23,137 @@ var webFS embed.FS
|
|||||||
var version = "dev"
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
initLogger()
|
||||||
|
slog.Info("VoicePaste", "version", version)
|
||||||
|
cfg := mustLoadConfig()
|
||||||
|
stopWatch := config.WatchAndReload("")
|
||||||
|
defer func() {
|
||||||
|
if stopWatch != nil {
|
||||||
|
stopWatch()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
initClipboard()
|
||||||
|
lanIPs := mustDetectLANIPs()
|
||||||
|
lanIP := lanIPs[0]
|
||||||
|
tlsResult := mustSetupTLS(lanIP)
|
||||||
|
printBanner(cfg, tlsResult, lanIPs)
|
||||||
|
srv := createServer(cfg, lanIP, tlsResult)
|
||||||
|
runWithGracefulShutdown(srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger() {
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
Level: slog.LevelInfo,
|
Level: slog.LevelInfo,
|
||||||
})))
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("VoicePaste", "version", version)
|
func mustLoadConfig() config.Config {
|
||||||
|
|
||||||
// Load config
|
|
||||||
cfg, err := config.Load("")
|
cfg, err := config.Load("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to load config", "error", err)
|
slog.Error("failed to load config", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
// Start config hot-reload watcher
|
func initClipboard() {
|
||||||
config.WatchAndReload("")
|
|
||||||
|
|
||||||
// Initialize clipboard
|
|
||||||
if err := paste.Init(); err != nil {
|
if err := paste.Init(); err != nil {
|
||||||
slog.Warn("clipboard init failed, paste will be unavailable", "err", err)
|
slog.Warn("clipboard init failed, paste will be unavailable", "err", err)
|
||||||
}
|
}
|
||||||
// Detect LAN IPs
|
}
|
||||||
|
|
||||||
|
func mustDetectLANIPs() []string {
|
||||||
lanIPs, err := server.GetLANIPs()
|
lanIPs, err := server.GetLANIPs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to detect LAN IP", "error", err)
|
slog.Error("failed to detect LAN IP", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
lanIP := lanIPs[0] // Use first IP for TLS and server binding
|
return lanIPs
|
||||||
|
}
|
||||||
|
|
||||||
// Read token from config (empty = no auth required)
|
func mustSetupTLS(lanIP string) *vpTLS.Result {
|
||||||
token := cfg.Security.Token
|
tlsResult, err := vpTLS.GetTLSConfig(lanIP)
|
||||||
|
if err != nil {
|
||||||
// TLS setup
|
slog.Error("TLS setup failed", "error", err)
|
||||||
var tlsResult *vpTLS.Result
|
os.Exit(1)
|
||||||
scheme := "http"
|
|
||||||
host := lanIP
|
|
||||||
if cfg.Server.TLSAuto {
|
|
||||||
var err error
|
|
||||||
tlsResult, err = vpTLS.GetTLSConfig(lanIP)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("TLS setup failed", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
scheme = "https"
|
|
||||||
host = tlsResult.Host
|
|
||||||
}
|
}
|
||||||
|
return tlsResult
|
||||||
|
}
|
||||||
|
|
||||||
// Print connection info
|
func printBanner(cfg config.Config, tlsResult *vpTLS.Result, lanIPs []string) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("╔══════════════════════════════════════╗")
|
fmt.Println("╔══════════════════════════════════════╗")
|
||||||
fmt.Println("║ VoicePaste 就绪 ║")
|
fmt.Println("║ VoicePaste 就绪 ║")
|
||||||
fmt.Println("╚══════════════════════════════════════╝")
|
fmt.Println("╚══════════════════════════════════════╝")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
// Print all accessible addresses
|
printAddresses(cfg, tlsResult, lanIPs)
|
||||||
|
printCertInfo(tlsResult)
|
||||||
|
printAuthInfo(cfg.Server.Token)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" 在手机浏览器中打开上方地址")
|
||||||
|
fmt.Println(" 按 Ctrl+C 停止服务")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAddresses(cfg config.Config, tlsResult *vpTLS.Result, lanIPs []string) {
|
||||||
|
token := cfg.Server.Token
|
||||||
if len(lanIPs) == 1 {
|
if len(lanIPs) == 1 {
|
||||||
fmt.Printf(" 地址: %s\n", buildURL(scheme, host, cfg.Server.Port, token))
|
host := lanIPHost(tlsResult, lanIPs[0])
|
||||||
} else {
|
fmt.Printf(" 地址: %s\n", buildURL(host, cfg.Server.Port, token))
|
||||||
fmt.Println(" 地址:")
|
return
|
||||||
for _, ip := range lanIPs {
|
|
||||||
h := ip
|
|
||||||
if tlsResult != nil && tlsResult.AnyIP {
|
|
||||||
h = vpTLS.AnyIPHost(ip)
|
|
||||||
}
|
|
||||||
fmt.Printf(" - %s\n", buildURL(scheme, h, cfg.Server.Port, token))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if tlsResult != nil && tlsResult.AnyIP {
|
fmt.Println(" 地址:")
|
||||||
|
for _, ip := range lanIPs {
|
||||||
|
host := lanIPHost(tlsResult, ip)
|
||||||
|
fmt.Printf(" - %s\n", buildURL(host, cfg.Server.Port, token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lanIPHost(tlsResult *vpTLS.Result, ip string) string {
|
||||||
|
if tlsResult != nil {
|
||||||
|
return vpTLS.AnyIPHost(ip)
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCertInfo(tlsResult *vpTLS.Result) {
|
||||||
|
if tlsResult != nil {
|
||||||
fmt.Println(" 证书: AnyIP(浏览器信任)")
|
fmt.Println(" 证书: AnyIP(浏览器信任)")
|
||||||
} else if cfg.Server.TLSAuto {
|
} else {
|
||||||
fmt.Println(" 证书: 自签名(浏览器会警告)")
|
fmt.Println(" 证书: 获取失败")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAuthInfo(token string) {
|
||||||
if token != "" {
|
if token != "" {
|
||||||
fmt.Println(" 认证: 已启用")
|
fmt.Println(" 认证: 已启用")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" 认证: 未启用(无需 token)")
|
fmt.Println(" 认证: 未启用(无需 token)")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
}
|
||||||
fmt.Println(" 在手机浏览器中打开上方地址")
|
|
||||||
fmt.Println(" 按 Ctrl+C 停止服务")
|
func createServer(cfg config.Config, lanIP string, tlsResult *vpTLS.Result) *server.Server {
|
||||||
fmt.Println()
|
|
||||||
// Create and start server
|
|
||||||
webContent, _ := fs.Sub(webFS, "web/dist")
|
webContent, _ := fs.Sub(webFS, "web/dist")
|
||||||
var serverTLSCfg *crypto_tls.Config
|
var tlsConfig *crypto_tls.Config
|
||||||
if tlsResult != nil {
|
if tlsResult != nil {
|
||||||
serverTLSCfg = tlsResult.Config
|
tlsConfig = tlsResult.Config
|
||||||
}
|
}
|
||||||
srv := server.New(token, lanIP, webContent, serverTLSCfg)
|
srv := server.New(cfg.Server.Token, lanIP, webContent, tlsConfig)
|
||||||
// Build ASR factory from config
|
asrFactory := buildASRFactory()
|
||||||
asrCfg := asr.Config{
|
wsHandler := ws.NewHandler(cfg.Server.Token, paste.Paste, asrFactory)
|
||||||
AppKey: cfg.Doubao.AppKey,
|
wsHandler.Register(srv.App())
|
||||||
AccessKey: cfg.Doubao.AccessKey,
|
return srv
|
||||||
ResourceID: cfg.Doubao.ResourceID,
|
}
|
||||||
}
|
|
||||||
asrFactory := func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
func buildASRFactory() func(chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
||||||
|
return func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
||||||
|
cfg := config.Get()
|
||||||
|
asrCfg := asr.Config{
|
||||||
|
AppID: cfg.Doubao.AppID,
|
||||||
|
AccessToken: cfg.Doubao.AccessToken,
|
||||||
|
ResourceID: cfg.Doubao.ResourceID,
|
||||||
|
Hotwords: cfg.ASR.Hotwords,
|
||||||
|
}
|
||||||
client, err := asr.Dial(asrCfg, resultCh)
|
client, err := asr.Dial(asrCfg, resultCh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -129,12 +168,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
return sendAudio, cleanup, nil
|
return sendAudio, cleanup, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register WebSocket handler
|
func runWithGracefulShutdown(srv *server.Server) {
|
||||||
wsHandler := ws.NewHandler(token, paste.Paste, asrFactory)
|
|
||||||
wsHandler.Register(srv.App())
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
go func() {
|
go func() {
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
@@ -142,15 +178,15 @@ func main() {
|
|||||||
slog.Info("shutting down...")
|
slog.Info("shutting down...")
|
||||||
srv.Shutdown()
|
srv.Shutdown()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := srv.Start(); err != nil {
|
if err := srv.Start(); err != nil {
|
||||||
slog.Error("server error", "error", err)
|
slog.Error("server error", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func buildURL(scheme, host string, port int, token string) string {
|
|
||||||
|
func buildURL(host string, port int, token string) string {
|
||||||
if token != "" {
|
if token != "" {
|
||||||
return fmt.Sprintf("%s://%s:%d/?token=%s", scheme, host, port, token)
|
return fmt.Sprintf("https://%s:%d/?token=%s", host, port, token)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s://%s:%d/", scheme, host, port)
|
return fmt.Sprintf("https://%s:%d/", host, port)
|
||||||
}
|
}
|
||||||
|
|||||||
399
web/app.ts
399
web/app.ts
@@ -1,399 +0,0 @@
|
|||||||
import "./style.css";
|
|
||||||
import audioProcessorUrl from "./audio-processor.ts?worker&url";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VoicePaste — Main application logic.
|
|
||||||
*
|
|
||||||
* Modules:
|
|
||||||
* 1. WebSocket client (token auth, reconnect)
|
|
||||||
* 2. Audio pipeline (getUserMedia → AudioWorklet → resample → WS binary)
|
|
||||||
* 3. Recording controls (touch/mouse, state machine)
|
|
||||||
* 4. History (localStorage, tap to re-send)
|
|
||||||
* 5. UI state management
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ── Types ──
|
|
||||||
interface HistoryItem {
|
|
||||||
text: string;
|
|
||||||
ts: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServerMessage {
|
|
||||||
type: "partial" | "final" | "pasted" | "error";
|
|
||||||
text?: string;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AppState {
|
|
||||||
ws: WebSocket | null;
|
|
||||||
connected: boolean;
|
|
||||||
recording: boolean;
|
|
||||||
pendingStart: boolean;
|
|
||||||
startCancelled: boolean;
|
|
||||||
audioCtx: AudioContext | null;
|
|
||||||
workletNode: AudioWorkletNode | null;
|
|
||||||
stream: MediaStream | null;
|
|
||||||
reconnectDelay: number;
|
|
||||||
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Constants ──
|
|
||||||
const TARGET_SAMPLE_RATE = 16000;
|
|
||||||
const WS_RECONNECT_BASE = 1000;
|
|
||||||
const WS_RECONNECT_MAX = 16000;
|
|
||||||
const HISTORY_KEY = "voicepaste_history";
|
|
||||||
const HISTORY_MAX = 50;
|
|
||||||
|
|
||||||
// ── DOM refs ──
|
|
||||||
function q(sel: string): HTMLElement {
|
|
||||||
const el = document.querySelector<HTMLElement>(sel);
|
|
||||||
if (!el) throw new Error(`Element not found: ${sel}`);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
const statusEl = q("#status");
|
|
||||||
const statusText = q("#status-text");
|
|
||||||
const previewText = q("#preview-text");
|
|
||||||
const previewBox = q("#preview");
|
|
||||||
const micBtn = q("#mic-btn") as HTMLButtonElement;
|
|
||||||
const historyList = q("#history-list");
|
|
||||||
const historyEmpty = q("#history-empty");
|
|
||||||
const clearHistoryBtn = document.querySelector<HTMLElement>("#clear-history");
|
|
||||||
|
|
||||||
// ── State ──
|
|
||||||
const state: AppState = {
|
|
||||||
ws: null,
|
|
||||||
connected: false,
|
|
||||||
recording: false,
|
|
||||||
pendingStart: false,
|
|
||||||
startCancelled: false,
|
|
||||||
audioCtx: null,
|
|
||||||
workletNode: null,
|
|
||||||
stream: null,
|
|
||||||
reconnectDelay: WS_RECONNECT_BASE,
|
|
||||||
reconnectTimer: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Utility ──
|
|
||||||
function getToken(): string {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
return params.get("token") || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ts: number): string {
|
|
||||||
const d = new Date(ts);
|
|
||||||
const hh = String(d.getHours()).padStart(2, "0");
|
|
||||||
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
||||||
return `${hh}:${mm}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Resampler (linear interpolation, native rate → 16kHz 16-bit mono) ──
|
|
||||||
function resampleTo16kInt16(
|
|
||||||
float32: Float32Array,
|
|
||||||
srcRate: number,
|
|
||||||
): Int16Array {
|
|
||||||
const ratio = srcRate / TARGET_SAMPLE_RATE;
|
|
||||||
const outLen = Math.floor(float32.length / ratio);
|
|
||||||
const out = new Int16Array(outLen);
|
|
||||||
for (let i = 0; i < outLen; i++) {
|
|
||||||
const srcIdx = i * ratio;
|
|
||||||
const lo = Math.floor(srcIdx);
|
|
||||||
const hi = Math.min(lo + 1, float32.length - 1);
|
|
||||||
const frac = srcIdx - lo;
|
|
||||||
const sample = float32[lo] + frac * (float32[hi] - float32[lo]);
|
|
||||||
// Clamp to [-1, 1] then scale to Int16
|
|
||||||
out[i] = Math.max(-32768, Math.min(32767, Math.round(sample * 32767)));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
// ── WebSocket ──
|
|
||||||
function wsUrl(): string {
|
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
const token = getToken();
|
|
||||||
const q = token ? `?token=${encodeURIComponent(token)}` : "";
|
|
||||||
return `${proto}//${location.host}/ws${q}`;
|
|
||||||
}
|
|
||||||
function setStatus(cls: string, text: string): void {
|
|
||||||
statusEl.className = `status ${cls}`;
|
|
||||||
statusText.textContent = text;
|
|
||||||
}
|
|
||||||
function connectWS(): void {
|
|
||||||
if (state.ws) return;
|
|
||||||
setStatus("connecting", "连接中…");
|
|
||||||
const ws = new WebSocket(wsUrl());
|
|
||||||
ws.binaryType = "arraybuffer";
|
|
||||||
ws.onopen = () => {
|
|
||||||
state.connected = true;
|
|
||||||
state.reconnectDelay = WS_RECONNECT_BASE;
|
|
||||||
setStatus("connected", "已连接");
|
|
||||||
micBtn.disabled = false;
|
|
||||||
};
|
|
||||||
ws.onmessage = (e: MessageEvent) => handleServerMsg(e.data);
|
|
||||||
ws.onclose = () => {
|
|
||||||
state.connected = false;
|
|
||||||
state.ws = null;
|
|
||||||
micBtn.disabled = true;
|
|
||||||
if (state.recording) stopRecording();
|
|
||||||
// Clean up pending async start on disconnect
|
|
||||||
if (state.pendingStart) {
|
|
||||||
state.pendingStart = false;
|
|
||||||
state.startCancelled = true;
|
|
||||||
micBtn.classList.remove("recording");
|
|
||||||
}
|
|
||||||
setStatus("disconnected", "已断开");
|
|
||||||
scheduleReconnect();
|
|
||||||
};
|
|
||||||
ws.onerror = () => ws.close();
|
|
||||||
state.ws = ws;
|
|
||||||
}
|
|
||||||
function scheduleReconnect(): void {
|
|
||||||
if (state.reconnectTimer !== null) clearTimeout(state.reconnectTimer);
|
|
||||||
state.reconnectTimer = setTimeout(() => {
|
|
||||||
connectWS();
|
|
||||||
}, state.reconnectDelay);
|
|
||||||
state.reconnectDelay = Math.min(state.reconnectDelay * 2, WS_RECONNECT_MAX);
|
|
||||||
}
|
|
||||||
function sendJSON(obj: Record<string, unknown>): void {
|
|
||||||
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
||||||
state.ws.send(JSON.stringify(obj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function sendBinary(int16arr: Int16Array): void {
|
|
||||||
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
||||||
state.ws.send(int16arr.buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ── Server message handler ──
|
|
||||||
function handleServerMsg(data: unknown): void {
|
|
||||||
if (typeof data !== "string") return;
|
|
||||||
let msg: ServerMessage;
|
|
||||||
try {
|
|
||||||
msg = JSON.parse(data) as ServerMessage;
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (msg.type) {
|
|
||||||
case "partial":
|
|
||||||
setPreview(msg.text || "", false);
|
|
||||||
break;
|
|
||||||
case "final":
|
|
||||||
setPreview(msg.text || "", true);
|
|
||||||
if (msg.text) addHistory(msg.text);
|
|
||||||
break;
|
|
||||||
case "pasted":
|
|
||||||
showToast("✅ 已粘贴");
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
showToast(`❌ ${msg.message || "错误"}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function setPreview(text: string, isFinal: boolean): void {
|
|
||||||
if (!text) {
|
|
||||||
previewText.textContent = "按住说话…";
|
|
||||||
previewText.classList.add("placeholder");
|
|
||||||
previewBox.classList.remove("active");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
previewText.textContent = text;
|
|
||||||
previewText.classList.remove("placeholder");
|
|
||||||
previewBox.classList.toggle("active", !isFinal);
|
|
||||||
}
|
|
||||||
function showToast(msg: string): void {
|
|
||||||
let toast = document.getElementById("toast");
|
|
||||||
if (!toast) {
|
|
||||||
toast = document.createElement("div");
|
|
||||||
toast.id = "toast";
|
|
||||||
toast.style.cssText =
|
|
||||||
"position:fixed;bottom:calc(100px + var(--safe-bottom,0px));left:50%;" +
|
|
||||||
"transform:translateX(-50%);background:#222;color:#eee;padding:8px 18px;" +
|
|
||||||
"border-radius:20px;font-size:14px;z-index:999;opacity:0;transition:opacity .3s;";
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
}
|
|
||||||
toast.textContent = msg;
|
|
||||||
toast.style.opacity = "1";
|
|
||||||
clearTimeout(
|
|
||||||
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer,
|
|
||||||
);
|
|
||||||
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer =
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = "0";
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
// ── Audio pipeline ──
|
|
||||||
async function initAudio(): Promise<void> {
|
|
||||||
if (state.audioCtx) return;
|
|
||||||
// Use device native sample rate — we resample to 16kHz in software
|
|
||||||
const audioCtx = new AudioContext();
|
|
||||||
// Chrome requires resume() after user gesture
|
|
||||||
if (audioCtx.state === "suspended") {
|
|
||||||
await audioCtx.resume();
|
|
||||||
}
|
|
||||||
await audioCtx.audioWorklet.addModule(audioProcessorUrl);
|
|
||||||
state.audioCtx = audioCtx;
|
|
||||||
}
|
|
||||||
async function startRecording(): Promise<void> {
|
|
||||||
if (state.recording || state.pendingStart) return;
|
|
||||||
state.pendingStart = true;
|
|
||||||
state.startCancelled = false;
|
|
||||||
try {
|
|
||||||
await initAudio();
|
|
||||||
if (state.startCancelled) {
|
|
||||||
state.pendingStart = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const audioCtx = state.audioCtx as AudioContext;
|
|
||||||
// Ensure AudioContext is running (may suspend between recordings)
|
|
||||||
if (audioCtx.state === "suspended") {
|
|
||||||
await audioCtx.resume();
|
|
||||||
}
|
|
||||||
if (state.startCancelled) {
|
|
||||||
state.pendingStart = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: {
|
|
||||||
echoCancellation: true,
|
|
||||||
noiseSuppression: true,
|
|
||||||
channelCount: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (state.startCancelled) {
|
|
||||||
stream.getTracks().forEach((t) => {
|
|
||||||
t.stop();
|
|
||||||
});
|
|
||||||
state.pendingStart = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.stream = stream;
|
|
||||||
const source = audioCtx.createMediaStreamSource(stream);
|
|
||||||
const worklet = new AudioWorkletNode(audioCtx, "audio-processor");
|
|
||||||
worklet.port.onmessage = (e: MessageEvent) => {
|
|
||||||
if (e.data.type === "audio") {
|
|
||||||
const int16 = resampleTo16kInt16(e.data.samples, e.data.sampleRate);
|
|
||||||
sendBinary(int16);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
source.connect(worklet);
|
|
||||||
worklet.port.postMessage({ command: "start" });
|
|
||||||
// Don't connect worklet to destination (no playback)
|
|
||||||
state.workletNode = worklet;
|
|
||||||
state.pendingStart = false;
|
|
||||||
state.recording = true;
|
|
||||||
sendJSON({ type: "start" });
|
|
||||||
micBtn.classList.add("recording");
|
|
||||||
setPreview("", false);
|
|
||||||
} catch (err) {
|
|
||||||
state.pendingStart = false;
|
|
||||||
showToast(`麦克风错误: ${(err as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function stopRecording(): void {
|
|
||||||
// Cancel pending async start if still initializing
|
|
||||||
if (state.pendingStart) {
|
|
||||||
state.startCancelled = true;
|
|
||||||
micBtn.classList.remove("recording");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.recording) return;
|
|
||||||
state.recording = false;
|
|
||||||
// Stop worklet
|
|
||||||
if (state.workletNode) {
|
|
||||||
state.workletNode.port.postMessage({ command: "stop" });
|
|
||||||
state.workletNode.disconnect();
|
|
||||||
state.workletNode = null;
|
|
||||||
}
|
|
||||||
// Stop mic stream
|
|
||||||
if (state.stream) {
|
|
||||||
state.stream.getTracks().forEach((t) => {
|
|
||||||
t.stop();
|
|
||||||
});
|
|
||||||
state.stream = null;
|
|
||||||
}
|
|
||||||
sendJSON({ type: "stop" });
|
|
||||||
micBtn.classList.remove("recording");
|
|
||||||
}
|
|
||||||
// ── History (localStorage) ──
|
|
||||||
function loadHistory(): HistoryItem[] {
|
|
||||||
try {
|
|
||||||
return JSON.parse(
|
|
||||||
localStorage.getItem(HISTORY_KEY) || "[]",
|
|
||||||
) as HistoryItem[];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function saveHistory(items: HistoryItem[]): void {
|
|
||||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(items));
|
|
||||||
}
|
|
||||||
function addHistory(text: string): void {
|
|
||||||
const items = loadHistory();
|
|
||||||
items.unshift({ text, ts: Date.now() });
|
|
||||||
if (items.length > HISTORY_MAX) items.length = HISTORY_MAX;
|
|
||||||
saveHistory(items);
|
|
||||||
renderHistory();
|
|
||||||
}
|
|
||||||
function clearHistory(): void {
|
|
||||||
localStorage.removeItem(HISTORY_KEY);
|
|
||||||
renderHistory();
|
|
||||||
}
|
|
||||||
function renderHistory(): void {
|
|
||||||
const items = loadHistory();
|
|
||||||
historyList.innerHTML = "";
|
|
||||||
if (!items.length) {
|
|
||||||
(historyEmpty as HTMLElement).style.display = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(historyEmpty as HTMLElement).style.display = "none";
|
|
||||||
for (const item of items) {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.innerHTML =
|
|
||||||
`<span class="hist-text">${escapeHtml(item.text)}</span>` +
|
|
||||||
`<span class="hist-time">${formatTime(item.ts)}</span>`;
|
|
||||||
li.addEventListener("click", () => {
|
|
||||||
sendJSON({ type: "paste", text: item.text });
|
|
||||||
showToast("发送粘贴…");
|
|
||||||
});
|
|
||||||
historyList.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function escapeHtml(s: string): string {
|
|
||||||
const d = document.createElement("div");
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
// ── Event bindings ──
|
|
||||||
function bindMicButton(): void {
|
|
||||||
// Pointer Events: unified touch + mouse, no double-trigger
|
|
||||||
micBtn.addEventListener("pointerdown", (e: PointerEvent) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
e.preventDefault();
|
|
||||||
startRecording();
|
|
||||||
});
|
|
||||||
micBtn.addEventListener("pointerup", (e: PointerEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
stopRecording();
|
|
||||||
});
|
|
||||||
micBtn.addEventListener("pointerleave", () => {
|
|
||||||
if (state.recording || state.pendingStart) stopRecording();
|
|
||||||
});
|
|
||||||
micBtn.addEventListener("pointercancel", () => {
|
|
||||||
if (state.recording || state.pendingStart) stopRecording();
|
|
||||||
});
|
|
||||||
// Prevent context menu on long press
|
|
||||||
micBtn.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
||||||
}
|
|
||||||
// ── Init ──
|
|
||||||
function init(): void {
|
|
||||||
micBtn.disabled = true;
|
|
||||||
bindMicButton();
|
|
||||||
if (clearHistoryBtn) {
|
|
||||||
clearHistoryBtn.addEventListener("click", clearHistory);
|
|
||||||
}
|
|
||||||
renderHistory();
|
|
||||||
connectWS();
|
|
||||||
}
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/**
|
|
||||||
* AudioWorklet processor for VoicePaste.
|
|
||||||
*
|
|
||||||
* Captures raw Float32 PCM from the microphone, accumulates samples into
|
|
||||||
* ~200ms frames, and posts them to the main thread for resampling + WS send.
|
|
||||||
*
|
|
||||||
* Communication:
|
|
||||||
* Main → Processor: { command: "start" | "stop" }
|
|
||||||
* Processor → Main: { type: "audio", samples: Float32Array, sampleRate: number }
|
|
||||||
*/
|
|
||||||
|
|
||||||
// AudioWorkletGlobalScope globals (not in standard lib)
|
|
||||||
declare const sampleRate: number;
|
|
||||||
declare class AudioWorkletProcessor {
|
|
||||||
readonly port: MessagePort;
|
|
||||||
constructor();
|
|
||||||
process(
|
|
||||||
inputs: Float32Array[][],
|
|
||||||
outputs: Float32Array[][],
|
|
||||||
parameters: Record<string, Float32Array>,
|
|
||||||
): boolean;
|
|
||||||
}
|
|
||||||
declare function registerProcessor(
|
|
||||||
name: string,
|
|
||||||
ctor: new () => AudioWorkletProcessor,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
class VoicePasteProcessor extends AudioWorkletProcessor {
|
|
||||||
private recording = false;
|
|
||||||
private buffer: Float32Array[] = [];
|
|
||||||
private bufferLen = 0;
|
|
||||||
private readonly frameSize: number;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// ~200ms worth of samples at current sample rate
|
|
||||||
this.frameSize = Math.floor(sampleRate * 0.2);
|
|
||||||
|
|
||||||
this.port.onmessage = (e: MessageEvent) => {
|
|
||||||
if (e.data.command === "start") {
|
|
||||||
this.recording = true;
|
|
||||||
this.buffer = [];
|
|
||||||
this.bufferLen = 0;
|
|
||||||
} else if (e.data.command === "stop") {
|
|
||||||
if (this.bufferLen > 0) {
|
|
||||||
this.flush();
|
|
||||||
}
|
|
||||||
this.recording = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
process(inputs: Float32Array[][]): boolean {
|
|
||||||
if (!this.recording) return true;
|
|
||||||
|
|
||||||
const input = inputs[0];
|
|
||||||
if (!input || !input[0]) return true;
|
|
||||||
|
|
||||||
const channelData = input[0];
|
|
||||||
this.buffer.push(new Float32Array(channelData));
|
|
||||||
this.bufferLen += channelData.length;
|
|
||||||
|
|
||||||
if (this.bufferLen >= this.frameSize) {
|
|
||||||
this.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private flush(): void {
|
|
||||||
const merged = new Float32Array(this.bufferLen);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of this.buffer) {
|
|
||||||
merged.set(chunk, offset);
|
|
||||||
offset += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.port.postMessage(
|
|
||||||
{ type: "audio", samples: merged, sampleRate: sampleRate },
|
|
||||||
[merged.buffer],
|
|
||||||
);
|
|
||||||
|
|
||||||
this.buffer = [];
|
|
||||||
this.bufferLen = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerProcessor("audio-processor", VoicePasteProcessor);
|
|
||||||
@@ -15,7 +15,21 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"nursery": {
|
||||||
|
"useSortedClasses": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
|
|||||||
810
web/bun.lock
810
web/bun.lock
@@ -4,14 +4,216 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
|
"@picovoice/web-voice-processor": "^4.0.9",
|
||||||
|
"partysocket": "^1.1.16",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"zustand": "^5.0.11",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.4",
|
"@biomejs/biome": "^2.4.4",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.29.0", "", { "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" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.29.1", "", { "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" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
|
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "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" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.6", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
|
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="],
|
||||||
|
|
||||||
|
"@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g=="],
|
||||||
|
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.29.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-replace-supers": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/template": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.29.0", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.29.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.6", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q=="],
|
||||||
|
|
||||||
|
"@babel/preset-env": ["@babel/preset-env@7.29.0", "", { "dependencies": { "@babel/compat-data": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.29.0", "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.6", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-class-static-block": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6", "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.29.0", "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-regenerator": "^0.6.6", "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w=="],
|
||||||
|
|
||||||
|
"@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "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" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
|
"@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="],
|
||||||
@@ -82,6 +284,38 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="],
|
||||||
|
|
||||||
|
"@picovoice/web-utils": ["@picovoice/web-utils@1.3.1", "", { "dependencies": { "commander": "^9.2.0" }, "bin": { "pvbase64": "scripts/base64.js" } }, "sha512-jcDqdULtTm+yJrnHDjg64hARup+Z4wNkYuXHNx6EM8+qZkweBq9UA6XJrHAlUkPnlkso4JWjaIKhz3x8vZcd3g=="],
|
||||||
|
|
||||||
|
"@picovoice/web-voice-processor": ["@picovoice/web-voice-processor@4.0.9", "", { "dependencies": { "@picovoice/web-utils": "=1.3.1" } }, "sha512-20pdkFjtuiojAdLIkNHXt4YgpRnlUePFW+gfkeCb+J+2XTRDGOI50+aJzL95p6QjDzGXsO7PZhlz7yDofOvZtg=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||||
@@ -132,34 +366,610 @@
|
|||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||||
|
|
||||||
|
"@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||||
|
|
||||||
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
|
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||||
|
|
||||||
|
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
|
|
||||||
|
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
||||||
|
|
||||||
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
|
"babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.15", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw=="],
|
||||||
|
|
||||||
|
"babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ=="],
|
||||||
|
|
||||||
|
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.6", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A=="],
|
||||||
|
|
||||||
|
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
|
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001775", "", {}, "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A=="],
|
||||||
|
|
||||||
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
|
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"core-js-compat": ["core-js-compat@3.48.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
|
|
||||||
|
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||||
|
|
||||||
|
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||||
|
|
||||||
|
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
||||||
|
|
||||||
|
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||||
|
|
||||||
|
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||||
|
|
||||||
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||||
|
|
||||||
|
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||||
|
|
||||||
|
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
|
|
||||||
|
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||||
|
|
||||||
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
|
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||||
|
|
||||||
|
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
|
||||||
|
|
||||||
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
|
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||||
|
|
||||||
|
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||||
|
|
||||||
|
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||||
|
|
||||||
|
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||||
|
|
||||||
|
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
|
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||||
|
|
||||||
|
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||||
|
|
||||||
|
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||||
|
|
||||||
|
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||||
|
|
||||||
|
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||||
|
|
||||||
|
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||||
|
|
||||||
|
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||||
|
|
||||||
|
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||||
|
|
||||||
|
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
|
||||||
|
|
||||||
|
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||||
|
|
||||||
|
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
|
||||||
|
|
||||||
|
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||||
|
|
||||||
|
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||||
|
|
||||||
|
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||||
|
|
||||||
|
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||||
|
|
||||||
|
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||||
|
|
||||||
|
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||||
|
|
||||||
|
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||||
|
|
||||||
|
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
|
||||||
|
|
||||||
|
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||||
|
|
||||||
|
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||||
|
|
||||||
|
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
|
||||||
|
|
||||||
|
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||||
|
|
||||||
|
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||||
|
|
||||||
|
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||||
|
|
||||||
|
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||||
|
|
||||||
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
|
"partysocket": ["partysocket@1.1.16", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||||
|
|
||||||
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
||||||
|
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
|
||||||
|
|
||||||
|
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
|
||||||
|
|
||||||
|
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||||
|
|
||||||
|
"regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="],
|
||||||
|
|
||||||
|
"regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
|
||||||
|
|
||||||
|
"regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||||
|
|
||||||
|
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||||
|
|
||||||
|
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
|
||||||
|
|
||||||
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
|
|
||||||
|
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"smob": ["smob@1.6.1", "", {}, "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
|
||||||
|
|
||||||
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
|
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||||
|
|
||||||
|
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||||
|
|
||||||
|
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||||
|
|
||||||
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||||
|
|
||||||
|
"strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="],
|
||||||
|
|
||||||
|
"tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="],
|
||||||
|
|
||||||
|
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||||
|
|
||||||
|
"type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="],
|
||||||
|
|
||||||
|
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||||
|
|
||||||
|
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||||
|
|
||||||
|
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||||
|
|
||||||
|
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
|
||||||
|
|
||||||
|
"unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="],
|
||||||
|
|
||||||
|
"unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="],
|
||||||
|
|
||||||
|
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
|
||||||
|
|
||||||
|
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
|
||||||
|
|
||||||
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
|
"upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
|
|
||||||
|
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||||
|
|
||||||
|
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||||
|
|
||||||
|
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
|
||||||
|
|
||||||
|
"workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="],
|
||||||
|
|
||||||
|
"workbox-broadcast-update": ["workbox-broadcast-update@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA=="],
|
||||||
|
|
||||||
|
"workbox-build": ["workbox-build@7.4.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.4.0", "workbox-broadcast-update": "7.4.0", "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-google-analytics": "7.4.0", "workbox-navigation-preload": "7.4.0", "workbox-precaching": "7.4.0", "workbox-range-requests": "7.4.0", "workbox-recipes": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0", "workbox-streams": "7.4.0", "workbox-sw": "7.4.0", "workbox-window": "7.4.0" } }, "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA=="],
|
||||||
|
|
||||||
|
"workbox-cacheable-response": ["workbox-cacheable-response@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ=="],
|
||||||
|
|
||||||
|
"workbox-core": ["workbox-core@7.4.0", "", {}, "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ=="],
|
||||||
|
|
||||||
|
"workbox-expiration": ["workbox-expiration@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw=="],
|
||||||
|
|
||||||
|
"workbox-google-analytics": ["workbox-google-analytics@7.4.0", "", { "dependencies": { "workbox-background-sync": "7.4.0", "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ=="],
|
||||||
|
|
||||||
|
"workbox-navigation-preload": ["workbox-navigation-preload@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w=="],
|
||||||
|
|
||||||
|
"workbox-precaching": ["workbox-precaching@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg=="],
|
||||||
|
|
||||||
|
"workbox-range-requests": ["workbox-range-requests@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw=="],
|
||||||
|
|
||||||
|
"workbox-recipes": ["workbox-recipes@7.4.0", "", { "dependencies": { "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-precaching": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ=="],
|
||||||
|
|
||||||
|
"workbox-routing": ["workbox-routing@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ=="],
|
||||||
|
|
||||||
|
"workbox-strategies": ["workbox-strategies@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg=="],
|
||||||
|
|
||||||
|
"workbox-streams": ["workbox-streams@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0" } }, "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg=="],
|
||||||
|
|
||||||
|
"workbox-sw": ["workbox-sw@7.4.0", "", {}, "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw=="],
|
||||||
|
|
||||||
|
"workbox-window": ["workbox-window@7.4.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.4.0" } }, "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
||||||
|
|
||||||
|
"@picovoice/web-utils/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-babel/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-replace/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="],
|
||||||
|
|
||||||
|
"@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||||
|
|
||||||
|
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||||
|
|
||||||
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
|
||||||
|
|
||||||
|
"workbox-build/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="theme-color" content="#08080d">
|
||||||
|
<meta name="description" content="语音转文字,一键粘贴到电脑剪贴板">
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<title>VoicePaste</title>
|
<title>VoicePaste</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-full bg-bg text-fg overflow-hidden select-none">
|
||||||
<div id="app">
|
<div id="root" class="h-full"></div>
|
||||||
<header>
|
<script type="module" src="src/main.tsx"></script>
|
||||||
<h1>VoicePaste</h1>
|
|
||||||
<div id="status" class="status disconnected">
|
|
||||||
<span class="dot"></span>
|
|
||||||
<span id="status-text">连接中…</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section id="preview-section">
|
|
||||||
<div id="preview" class="preview-box">
|
|
||||||
<p id="preview-text" class="placeholder">按住说话…</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="mic-section">
|
|
||||||
<button id="mic-btn" type="button" disabled>
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor" aria-label="麦克风" role="img">
|
|
||||||
<title>麦克风</title>
|
|
||||||
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
|
|
||||||
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="history-section">
|
|
||||||
<div class="history-header">
|
|
||||||
<h2>历史记录</h2>
|
|
||||||
<button id="clear-history" type="button" class="text-btn">清空</button>
|
|
||||||
</div>
|
|
||||||
<ul id="history-list"></ul>
|
|
||||||
<p id="history-empty" class="placeholder">暂无记录</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="app.ts"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,7 +11,23 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.4",
|
"@biomejs/biome": "^2.4.4",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-pwa": "^1.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
|
"@picovoice/web-voice-processor": "^4.0.9",
|
||||||
|
"partysocket": "^1.1.16",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
13
web/public/favicon.svg
Normal file
13
web/public/favicon.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="96" fill="#08080d"/>
|
||||||
|
<g transform="translate(256,232)">
|
||||||
|
<!-- Mic body -->
|
||||||
|
<rect x="-48" y="-100" width="96" height="160" rx="48" fill="#6366f1"/>
|
||||||
|
<!-- Mic arc -->
|
||||||
|
<path d="M-72,30 a72,72 0 0,0 144,0" fill="none" stroke="#6366f1" stroke-width="16" stroke-linecap="round"/>
|
||||||
|
<!-- Mic stem -->
|
||||||
|
<line x1="0" y1="102" x2="0" y2="148" stroke="#6366f1" stroke-width="16" stroke-linecap="round"/>
|
||||||
|
<!-- Mic base -->
|
||||||
|
<line x1="-36" y1="148" x2="36" y2="148" stroke="#6366f1" stroke-width="16" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
BIN
web/public/pwa-192x192.png
Normal file
BIN
web/public/pwa-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
web/public/pwa-512x512.png
Normal file
BIN
web/public/pwa-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
66
web/src/App.tsx
Normal file
66
web/src/App.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
import { HistoryList } from "./components/HistoryList";
|
||||||
|
import { MicButton } from "./components/MicButton";
|
||||||
|
import { PreviewBox } from "./components/PreviewBox";
|
||||||
|
import { StatusBadge } from "./components/StatusBadge";
|
||||||
|
import { useRecorder } from "./hooks/useRecorder";
|
||||||
|
import { useWebSocket } from "./hooks/useWebSocket";
|
||||||
|
import { useAppStore } from "./stores/app-store";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const { requestStart, sendStop, sendPaste, sendAudioFrame } = useWebSocket();
|
||||||
|
const { prewarm, startRecording, stopRecording } = useRecorder({
|
||||||
|
requestStart,
|
||||||
|
sendStop,
|
||||||
|
sendAudioFrame,
|
||||||
|
});
|
||||||
|
const micReady = useAppStore((s) => s.micReady);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const forceStopOnBackground = () => {
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
if (state.recording || state.pendingStart) {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVisibility = () => {
|
||||||
|
if (document.hidden) forceStopOnBackground();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", onVisibility);
|
||||||
|
window.addEventListener("pagehide", forceStopOnBackground);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", onVisibility);
|
||||||
|
window.removeEventListener("pagehide", forceStopOnBackground);
|
||||||
|
};
|
||||||
|
}, [stopRecording]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative z-1 mx-auto flex h-dvh max-w-[480px] flex-col px-5 pt-[calc(16px+env(safe-area-inset-top,0px))] pb-[calc(16px+env(safe-area-inset-bottom,0px))]">
|
||||||
|
<header className="flex shrink-0 items-center justify-between pt-2 pb-5">
|
||||||
|
<h1 className="font-bold text-[22px] tracking-[-0.03em]">
|
||||||
|
VoicePaste
|
||||||
|
</h1>
|
||||||
|
<StatusBadge />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<PreviewBox />
|
||||||
|
{!micReady ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void prewarm()}
|
||||||
|
className="mb-3 rounded-xl border border-edge bg-surface px-4 py-3 font-medium text-fg text-sm"
|
||||||
|
>
|
||||||
|
{"先点我准备麦克风(首次会弹权限)"}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<MicButton onStart={startRecording} onStop={stopRecording} />
|
||||||
|
<HistoryList sendPaste={sendPaste} />
|
||||||
|
</div>
|
||||||
|
<Toaster theme="dark" position="bottom-center" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
web/src/app.css
Normal file
112
web/src/app.css
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ─── Design Tokens ─── */
|
||||||
|
@theme {
|
||||||
|
--color-bg: #08080d;
|
||||||
|
--color-surface: #111117;
|
||||||
|
--color-surface-hover: #17171e;
|
||||||
|
--color-surface-active: #1c1c25;
|
||||||
|
--color-edge: #1e1e2a;
|
||||||
|
--color-edge-active: #2c2c3e;
|
||||||
|
--color-fg: #eaeaef;
|
||||||
|
--color-fg-secondary: #9e9eb5;
|
||||||
|
--color-fg-dim: #5a5a6e;
|
||||||
|
--color-accent: #6366f1;
|
||||||
|
--color-accent-hover: #818cf8;
|
||||||
|
--color-danger: #f43f5e;
|
||||||
|
--color-success: #34d399;
|
||||||
|
--radius-card: 14px;
|
||||||
|
|
||||||
|
--animate-pulse-dot: pulse-dot 1.4s ease-in-out infinite;
|
||||||
|
--animate-mic-breathe: mic-breathe 1.8s ease-in-out infinite;
|
||||||
|
--animate-ring-expand: ring-expand 2.4s cubic-bezier(0.2, 0, 0.2, 1) infinite;
|
||||||
|
--animate-slide-up: slide-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Base ─── */
|
||||||
|
@layer base {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
"SF Pro Display", -apple-system, BlinkMacSystemFont, "PingFang SC",
|
||||||
|
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
content: "";
|
||||||
|
@apply pointer-events-none fixed z-0;
|
||||||
|
top: -30%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(99, 102, 241, 0.04) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Scrollbar ─── */
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-edge;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Keyframes ─── */
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mic-breathe {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 32px rgba(99, 102, 241, 0.35),
|
||||||
|
0 0 80px rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 48px rgba(99, 102, 241, 0.35),
|
||||||
|
0 0 120px rgba(99, 102, 241, 0.2),
|
||||||
|
0 0 200px rgba(99, 102, 241, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring-expand {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
web/src/components/HistoryList.tsx
Normal file
65
web/src/components/HistoryList.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
function formatTime(ts: number): string {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryListProps {
|
||||||
|
sendPaste: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryList({ sendPaste }: HistoryListProps) {
|
||||||
|
const history = useAppStore((s) => s.history);
|
||||||
|
const clearHistory = useAppStore((s) => s.clearHistory);
|
||||||
|
|
||||||
|
const handleItemClick = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
sendPaste(text);
|
||||||
|
toast.info("发送粘贴…");
|
||||||
|
},
|
||||||
|
[sendPaste],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
|
<div className="flex shrink-0 items-center justify-between pb-2.5">
|
||||||
|
<h2 className="font-semibold text-[13px] text-fg-dim uppercase tracking-[0.06em]">
|
||||||
|
{"历史记录"}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearHistory}
|
||||||
|
className="cursor-pointer rounded-lg border-none bg-transparent px-2.5 py-1 font-medium text-fg-dim text-xs transition-all duration-150 active:bg-danger/[0.08] active:text-danger"
|
||||||
|
>
|
||||||
|
{"清空"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{history.length === 0 ? (
|
||||||
|
<p className="py-10 text-center text-fg-dim text-sm">{"暂无记录"}</p>
|
||||||
|
) : (
|
||||||
|
<div className="scrollbar-thin flex-1 overflow-y-auto overscroll-contain">
|
||||||
|
{history.map((item, i) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={`${item.ts}-${i}`}
|
||||||
|
onClick={() => handleItemClick(item.text)}
|
||||||
|
className="mb-2 flex w-full animate-slide-up cursor-pointer items-start gap-3 rounded-card border border-edge bg-surface px-4 py-3.5 text-left text-sm leading-relaxed transition-all duration-150 active:scale-[0.985] active:border-edge-active active:bg-surface-active"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${Math.min(i, 10) * 40}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex-1 break-words">{item.text}</span>
|
||||||
|
<span className="shrink-0 whitespace-nowrap pt-0.5 text-[11px] text-fg-dim tabular-nums">
|
||||||
|
{formatTime(item.ts)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
web/src/components/MicButton.tsx
Normal file
122
web/src/components/MicButton.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
interface MicButtonProps {
|
||||||
|
onStart: () => void;
|
||||||
|
onStop: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MicButton({ onStart, onStop }: MicButtonProps) {
|
||||||
|
const connected = useAppStore((s) => s.connectionStatus === "connected");
|
||||||
|
const micReady = useAppStore((s) => s.micReady);
|
||||||
|
const recording = useAppStore((s) => s.recording);
|
||||||
|
const pendingStart = useAppStore((s) => s.pendingStart);
|
||||||
|
const stopping = useAppStore((s) => s.stopping);
|
||||||
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const isActive = recording || pendingStart || stopping;
|
||||||
|
const disabled = !connected || !micReady || stopping;
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
onStart();
|
||||||
|
},
|
||||||
|
[onStart],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePointerUp = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
onStop();
|
||||||
|
},
|
||||||
|
[onStop],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read latest state to avoid stale closures
|
||||||
|
const handlePointerLeave = useCallback(() => {
|
||||||
|
const s = useAppStore.getState();
|
||||||
|
if (s.recording || s.pendingStart) onStop();
|
||||||
|
}, [onStop]);
|
||||||
|
|
||||||
|
const handlePointerCancel = useCallback(() => {
|
||||||
|
const s = useAppStore.getState();
|
||||||
|
if (s.recording || s.pendingStart) onStop();
|
||||||
|
}, [onStop]);
|
||||||
|
|
||||||
|
const disabledClasses =
|
||||||
|
"cursor-not-allowed border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary opacity-30 shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
||||||
|
const activeClasses =
|
||||||
|
"animate-mic-breathe scale-[1.06] border-accent-hover bg-accent text-white shadow-[0_0_32px_rgba(99,102,241,0.35),0_0_80px_rgba(99,102,241,0.2)]";
|
||||||
|
const weakClasses =
|
||||||
|
"border-amber-400 bg-linear-to-br from-amber-400/15 to-surface text-amber-200 shadow-[0_0_22px_rgba(251,191,36,0.28)]";
|
||||||
|
const idleClasses =
|
||||||
|
"border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex shrink-0 flex-col items-center gap-3.5 pt-5 pb-4">
|
||||||
|
<div className="relative flex touch-none items-center justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className={`relative z-1 flex size-24 cursor-pointer touch-none select-none items-center justify-center rounded-full border-2 transition-all duration-[250ms] ease-[cubic-bezier(0.4,0,0.2,1)] ${
|
||||||
|
disabled
|
||||||
|
? disabledClasses
|
||||||
|
: weakNetwork
|
||||||
|
? weakClasses
|
||||||
|
: isActive
|
||||||
|
? activeClasses
|
||||||
|
: idleClasses
|
||||||
|
}`}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerLeave={handlePointerLeave}
|
||||||
|
onPointerCancel={handlePointerCancel}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
fill="currentColor"
|
||||||
|
aria-label="麦克风"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<title>{"麦克风"}</title>
|
||||||
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
|
||||||
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Wave rings */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{[0, 0.8, 1.6].map((delay) => (
|
||||||
|
<span
|
||||||
|
key={delay}
|
||||||
|
className={`absolute inset-0 rounded-full border-[1.5px] border-accent ${
|
||||||
|
isActive ? "animate-ring-expand" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
style={isActive ? { animationDelay: `${delay}s` } : undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-fg-dim text-sm">
|
||||||
|
{!micReady
|
||||||
|
? "请先准备麦克风"
|
||||||
|
: !connected
|
||||||
|
? "连接中断,等待重连"
|
||||||
|
: stopping
|
||||||
|
? "收尾中…"
|
||||||
|
: weakNetwork
|
||||||
|
? "网络波动,已启用缓冲"
|
||||||
|
: "按住说话"}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
web/src/components/PreviewBox.tsx
Normal file
29
web/src/components/PreviewBox.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
export function PreviewBox() {
|
||||||
|
const text = useAppStore((s) => s.previewText);
|
||||||
|
const active = useAppStore((s) => s.previewActive);
|
||||||
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const recording = useAppStore((s) => s.recording);
|
||||||
|
const hasText = text.length > 0;
|
||||||
|
const placeholder =
|
||||||
|
weakNetwork && recording ? "网络波动中,音频缓冲后发送…" : "按住说话…";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="shrink-0 pb-3">
|
||||||
|
<div
|
||||||
|
className={`scrollbar-thin max-h-40 min-h-20 overflow-y-auto rounded-card border px-[18px] py-4 transition-all duration-300 ${
|
||||||
|
active
|
||||||
|
? "border-accent/40 bg-linear-to-b from-accent/[0.03] to-surface shadow-[0_0_0_1px_rgba(99,102,241,0.2),0_4px_24px_-4px_rgba(99,102,241,0.15)]"
|
||||||
|
: "border-edge bg-surface"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={`break-words text-base leading-relaxed ${hasText ? "" : "text-fg-dim"}`}
|
||||||
|
>
|
||||||
|
{hasText ? text : placeholder}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
web/src/components/StatusBadge.tsx
Normal file
40
web/src/components/StatusBadge.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
connected: {
|
||||||
|
text: "已连接",
|
||||||
|
dotClass: "bg-success shadow-[0_0_6px_rgba(52,211,153,0.5)]",
|
||||||
|
borderClass: "border-success/15",
|
||||||
|
},
|
||||||
|
disconnected: {
|
||||||
|
text: "已断开",
|
||||||
|
dotClass: "bg-danger shadow-[0_0_6px_rgba(244,63,94,0.4)]",
|
||||||
|
borderClass: "border-edge",
|
||||||
|
},
|
||||||
|
connecting: {
|
||||||
|
text: "连接中…",
|
||||||
|
dotClass: "bg-accent animate-pulse-dot",
|
||||||
|
borderClass: "border-edge",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function StatusBadge() {
|
||||||
|
const status = useAppStore((s) => s.connectionStatus);
|
||||||
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
||||||
|
const { dotClass, borderClass } = statusConfig[status];
|
||||||
|
const text =
|
||||||
|
status === "connected" && weakNetwork
|
||||||
|
? "网络波动"
|
||||||
|
: statusConfig[status].text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-[7px] rounded-full border bg-surface px-3 py-[5px] font-medium text-fg-dim text-xs transition-all ${borderClass}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`size-[7px] shrink-0 rounded-full transition-all duration-300 ${dotClass}`}
|
||||||
|
/>
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
web/src/hooks/useRecorder.ts
Normal file
160
web/src/hooks/useRecorder.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { WebVoiceProcessor } from "@picovoice/web-voice-processor";
|
||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
const FRAME_LENGTH = 3200;
|
||||||
|
|
||||||
|
interface UseRecorderOptions {
|
||||||
|
requestStart: () => Promise<string | null>;
|
||||||
|
sendStop: (sessionId: string | null) => void;
|
||||||
|
sendAudioFrame: (sessionId: string, seq: number, data: Int16Array) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionsInitialized = false;
|
||||||
|
|
||||||
|
export function useRecorder({
|
||||||
|
requestStart,
|
||||||
|
sendStop,
|
||||||
|
sendAudioFrame,
|
||||||
|
}: UseRecorderOptions) {
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
const engineRef = useRef<{ onmessage: (e: MessageEvent) => void } | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const warmedRef = useRef(false);
|
||||||
|
const audioSeqRef = useRef(1);
|
||||||
|
|
||||||
|
const initOptions = useCallback(() => {
|
||||||
|
if (optionsInitialized) return;
|
||||||
|
WebVoiceProcessor.setOptions({
|
||||||
|
frameLength: FRAME_LENGTH,
|
||||||
|
outputSampleRate: 16000,
|
||||||
|
});
|
||||||
|
optionsInitialized = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const prewarm = useCallback(async (): Promise<boolean> => {
|
||||||
|
if (warmedRef.current) return true;
|
||||||
|
initOptions();
|
||||||
|
|
||||||
|
const warmEngine = {
|
||||||
|
onmessage: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await WebVoiceProcessor.subscribe(warmEngine);
|
||||||
|
await WebVoiceProcessor.unsubscribe(warmEngine);
|
||||||
|
warmedRef.current = true;
|
||||||
|
useAppStore.getState().setMicReady(true);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
toast.error(`麦克风准备失败: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [initOptions]);
|
||||||
|
|
||||||
|
const startRecording = useCallback(async () => {
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
if (store.recording || store.pendingStart || store.stopping) return;
|
||||||
|
|
||||||
|
store.setPendingStart(true);
|
||||||
|
store.setStopping(false);
|
||||||
|
|
||||||
|
const abort = new AbortController();
|
||||||
|
abortRef.current = abort;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const warmed = await prewarm();
|
||||||
|
if (!warmed) {
|
||||||
|
store.setPendingStart(false);
|
||||||
|
abortRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = await requestStart();
|
||||||
|
if (!sessionId) {
|
||||||
|
store.setPendingStart(false);
|
||||||
|
abortRef.current = null;
|
||||||
|
toast.error("启动会话失败,请重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = {
|
||||||
|
onmessage: (e: MessageEvent) => {
|
||||||
|
if (e.data.command !== "process") return;
|
||||||
|
const seq = audioSeqRef.current;
|
||||||
|
audioSeqRef.current += 1;
|
||||||
|
sendAudioFrame(sessionId, seq, e.data.inputFrame as Int16Array);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
engineRef.current = engine;
|
||||||
|
audioSeqRef.current = 1;
|
||||||
|
|
||||||
|
await WebVoiceProcessor.subscribe(engine);
|
||||||
|
|
||||||
|
if (abort.signal.aborted) {
|
||||||
|
await WebVoiceProcessor.unsubscribe(engine);
|
||||||
|
engineRef.current = null;
|
||||||
|
sendStop(sessionId);
|
||||||
|
store.setPendingStart(false);
|
||||||
|
abortRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setActiveSessionId(sessionId);
|
||||||
|
store.setPendingStart(false);
|
||||||
|
store.setRecording(true);
|
||||||
|
store.setStopping(false);
|
||||||
|
store.clearPreview();
|
||||||
|
abortRef.current = null;
|
||||||
|
} catch (err) {
|
||||||
|
store.setPendingStart(false);
|
||||||
|
store.setRecording(false);
|
||||||
|
store.setStopping(false);
|
||||||
|
abortRef.current = null;
|
||||||
|
engineRef.current = null;
|
||||||
|
|
||||||
|
const error = err as Error;
|
||||||
|
switch (error.name) {
|
||||||
|
case "PermissionError":
|
||||||
|
toast.error("麦克风权限被拒绝");
|
||||||
|
break;
|
||||||
|
case "DeviceMissingError":
|
||||||
|
toast.error("未找到麦克风设备");
|
||||||
|
break;
|
||||||
|
case "DeviceReadError":
|
||||||
|
toast.error("麦克风设备异常,请检查连接");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.error(`麦克风错误: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [prewarm, requestStart, sendAudioFrame, sendStop]);
|
||||||
|
|
||||||
|
const stopRecording = useCallback(() => {
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
|
||||||
|
if (store.pendingStart) {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
abortRef.current = null;
|
||||||
|
store.setPendingStart(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store.recording) return;
|
||||||
|
store.setRecording(false);
|
||||||
|
store.setStopping(true);
|
||||||
|
audioSeqRef.current = 1;
|
||||||
|
|
||||||
|
if (engineRef.current) {
|
||||||
|
WebVoiceProcessor.unsubscribe(engineRef.current);
|
||||||
|
engineRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStop(store.activeSessionId);
|
||||||
|
}, [sendStop]);
|
||||||
|
|
||||||
|
return { prewarm, startRecording, stopRecording };
|
||||||
|
}
|
||||||
388
web/src/hooks/useWebSocket.ts
Normal file
388
web/src/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import { encode } from "@msgpack/msgpack";
|
||||||
|
import { WebSocket as ReconnectingWebSocket } from "partysocket";
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { ClientMsg, ServerMsg } from "../protocol";
|
||||||
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL_MS = 15000;
|
||||||
|
const HEARTBEAT_TIMEOUT_MS = 10000;
|
||||||
|
const START_TIMEOUT_MS = 5000;
|
||||||
|
const AUDIO_PACKET_VERSION = 1;
|
||||||
|
const MAX_AUDIO_QUEUE = 6;
|
||||||
|
const WEAK_NETWORK_TOAST_INTERVAL_MS = 4000;
|
||||||
|
const WS_BACKPRESSURE_BYTES = 128 * 1024;
|
||||||
|
|
||||||
|
interface QueuedAudioPacket {
|
||||||
|
sessionId: string;
|
||||||
|
seq: number;
|
||||||
|
buffer: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AudioPacketPayload {
|
||||||
|
v: number;
|
||||||
|
sessionId: string;
|
||||||
|
seq: number;
|
||||||
|
pcm: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWsUrl(): string {
|
||||||
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const token = params.get("token") || "";
|
||||||
|
const q = token ? `?token=${encodeURIComponent(token)}` : "";
|
||||||
|
return `${proto}//${location.host}/ws${q}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionId(): string {
|
||||||
|
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAudioPacket(
|
||||||
|
sessionId: string,
|
||||||
|
seq: number,
|
||||||
|
pcm: Int16Array,
|
||||||
|
): ArrayBuffer {
|
||||||
|
const sidBytes = new TextEncoder().encode(sessionId);
|
||||||
|
if (sidBytes.length === 0 || sidBytes.length > 96) {
|
||||||
|
throw new Error("invalid sessionId length");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pcmBytes = new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
||||||
|
const payload: AudioPacketPayload = {
|
||||||
|
v: AUDIO_PACKET_VERSION,
|
||||||
|
sessionId,
|
||||||
|
seq,
|
||||||
|
pcm: pcmBytes,
|
||||||
|
};
|
||||||
|
const packed = encode(payload);
|
||||||
|
return packed.buffer.slice(
|
||||||
|
packed.byteOffset,
|
||||||
|
packed.byteOffset + packed.byteLength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWebSocket() {
|
||||||
|
const wsRef = useRef<ReconnectingWebSocket | null>(null);
|
||||||
|
const heartbeatTimerRef = useRef<number | null>(null);
|
||||||
|
const heartbeatTimeoutRef = useRef<number | null>(null);
|
||||||
|
const pendingStartRef = useRef<{
|
||||||
|
sessionId: string;
|
||||||
|
resolve: (sessionId: string | null) => void;
|
||||||
|
timer: number;
|
||||||
|
} | null>(null);
|
||||||
|
const audioQueueRef = useRef<QueuedAudioPacket[]>([]);
|
||||||
|
const lastWeakToastAtRef = useRef(0);
|
||||||
|
|
||||||
|
const clearHeartbeat = useCallback(() => {
|
||||||
|
if (heartbeatTimerRef.current !== null) {
|
||||||
|
window.clearInterval(heartbeatTimerRef.current);
|
||||||
|
heartbeatTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (heartbeatTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(heartbeatTimeoutRef.current);
|
||||||
|
heartbeatTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendControl = useCallback((msg: ClientMsg): boolean => {
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const notifyWeakNetwork = useCallback(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastWeakToastAtRef.current < WEAK_NETWORK_TOAST_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWeakToastAtRef.current = now;
|
||||||
|
toast.warning("网络波动,正在缓冲音频…");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const flushAudioQueue = useCallback(() => {
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (ws?.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
const activeSessionId = useAppStore.getState().activeSessionId;
|
||||||
|
if (!activeSessionId) {
|
||||||
|
audioQueueRef.current = [];
|
||||||
|
useAppStore.getState().setWeakNetwork(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining: QueuedAudioPacket[] = [];
|
||||||
|
for (const packet of audioQueueRef.current) {
|
||||||
|
if (packet.sessionId !== activeSessionId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
ws.readyState !== WebSocket.OPEN ||
|
||||||
|
ws.bufferedAmount > WS_BACKPRESSURE_BYTES
|
||||||
|
) {
|
||||||
|
remaining.push(packet);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ws.send(packet.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
audioQueueRef.current = remaining;
|
||||||
|
if (audioQueueRef.current.length === 0) {
|
||||||
|
useAppStore.getState().setWeakNetwork(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendAudioFrame = useCallback(
|
||||||
|
(sessionId: string, seq: number, pcm: Int16Array): boolean => {
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
if (!store.recording || store.activeSessionId !== sessionId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer: ArrayBuffer;
|
||||||
|
try {
|
||||||
|
buffer = encodeAudioPacket(sessionId, seq, pcm);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (
|
||||||
|
ws?.readyState === WebSocket.OPEN &&
|
||||||
|
ws.bufferedAmount <= WS_BACKPRESSURE_BYTES
|
||||||
|
) {
|
||||||
|
ws.send(buffer);
|
||||||
|
if (audioQueueRef.current.length === 0) {
|
||||||
|
useAppStore.getState().setWeakNetwork(false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioQueueRef.current.length >= MAX_AUDIO_QUEUE) {
|
||||||
|
audioQueueRef.current.shift();
|
||||||
|
}
|
||||||
|
audioQueueRef.current.push({ sessionId, seq, buffer });
|
||||||
|
useAppStore.getState().setWeakNetwork(true);
|
||||||
|
notifyWeakNetwork();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[notifyWeakNetwork],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvePendingStart = useCallback((sessionId: string | null) => {
|
||||||
|
const pending = pendingStartRef.current;
|
||||||
|
if (!pending) return;
|
||||||
|
window.clearTimeout(pending.timer);
|
||||||
|
pending.resolve(sessionId);
|
||||||
|
pendingStartRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleServerMessage = useCallback(
|
||||||
|
(raw: string) => {
|
||||||
|
let msg: ServerMsg;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(raw) as ServerMsg;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
switch (msg.type) {
|
||||||
|
case "ready":
|
||||||
|
break;
|
||||||
|
case "state":
|
||||||
|
if (msg.state === "idle") {
|
||||||
|
store.setRecording(false);
|
||||||
|
store.setStopping(false);
|
||||||
|
store.setPendingStart(false);
|
||||||
|
store.setActiveSessionId(null);
|
||||||
|
}
|
||||||
|
if (msg.state === "stopping") {
|
||||||
|
store.setStopping(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "start_ack":
|
||||||
|
if (
|
||||||
|
pendingStartRef.current &&
|
||||||
|
msg.sessionId === pendingStartRef.current.sessionId
|
||||||
|
) {
|
||||||
|
resolvePendingStart(msg.sessionId ?? null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "stop_ack":
|
||||||
|
store.setStopping(true);
|
||||||
|
break;
|
||||||
|
case "partial": {
|
||||||
|
const activeSessionId = store.activeSessionId;
|
||||||
|
if (!activeSessionId || msg.sessionId !== activeSessionId) break;
|
||||||
|
store.setPreview(msg.text || "", false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "final": {
|
||||||
|
const activeSessionId = store.activeSessionId;
|
||||||
|
if (!activeSessionId || msg.sessionId !== activeSessionId) break;
|
||||||
|
store.setPreview(msg.text || "", true);
|
||||||
|
if (msg.text) store.addHistory(msg.text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "pasted":
|
||||||
|
toast.success("已粘贴");
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
store.setRecording(false);
|
||||||
|
store.setPendingStart(false);
|
||||||
|
store.setStopping(false);
|
||||||
|
resolvePendingStart(null);
|
||||||
|
toast.error(msg.message || "服务错误");
|
||||||
|
break;
|
||||||
|
case "pong":
|
||||||
|
if (heartbeatTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(heartbeatTimeoutRef.current);
|
||||||
|
heartbeatTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[resolvePendingStart],
|
||||||
|
);
|
||||||
|
|
||||||
|
const startHeartbeat = useCallback(() => {
|
||||||
|
clearHeartbeat();
|
||||||
|
heartbeatTimerRef.current = window.setInterval(() => {
|
||||||
|
if (document.hidden) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (!sendControl({ type: "ping", ts: now })) return;
|
||||||
|
if (heartbeatTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(heartbeatTimeoutRef.current);
|
||||||
|
}
|
||||||
|
heartbeatTimeoutRef.current = window.setTimeout(() => {
|
||||||
|
wsRef.current?.close();
|
||||||
|
}, HEARTBEAT_TIMEOUT_MS);
|
||||||
|
}, HEARTBEAT_INTERVAL_MS);
|
||||||
|
}, [clearHeartbeat, sendControl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ws = new ReconnectingWebSocket(getWsUrl(), undefined, {
|
||||||
|
minReconnectionDelay: 400,
|
||||||
|
maxReconnectionDelay: 6000,
|
||||||
|
});
|
||||||
|
ws.binaryType = "arraybuffer";
|
||||||
|
wsRef.current = ws;
|
||||||
|
useAppStore.getState().setConnectionStatus("connecting");
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
store.setConnectionStatus("connected");
|
||||||
|
store.setWeakNetwork(false);
|
||||||
|
sendControl({ type: "hello", version: 2 });
|
||||||
|
startHeartbeat();
|
||||||
|
flushAudioQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (e: MessageEvent) => {
|
||||||
|
if (typeof e.data === "string") {
|
||||||
|
handleServerMessage(e.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
clearHeartbeat();
|
||||||
|
resolvePendingStart(null);
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
if (store.recording || store.pendingStart) {
|
||||||
|
toast.warning("连接中断,本次录音已结束");
|
||||||
|
}
|
||||||
|
store.setConnectionStatus("disconnected");
|
||||||
|
store.setWeakNetwork(store.recording || audioQueueRef.current.length > 0);
|
||||||
|
store.setRecording(false);
|
||||||
|
store.setPendingStart(false);
|
||||||
|
store.setStopping(false);
|
||||||
|
store.setActiveSessionId(null);
|
||||||
|
audioQueueRef.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (!document.hidden && wsRef.current?.readyState !== WebSocket.OPEN) {
|
||||||
|
wsRef.current?.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||||
|
clearHeartbeat();
|
||||||
|
resolvePendingStart(null);
|
||||||
|
wsRef.current?.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
clearHeartbeat,
|
||||||
|
flushAudioQueue,
|
||||||
|
handleServerMessage,
|
||||||
|
resolvePendingStart,
|
||||||
|
sendControl,
|
||||||
|
startHeartbeat,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const requestStart = useCallback((): Promise<string | null> => {
|
||||||
|
const sessionId = createSessionId();
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
if (pendingStartRef.current) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
if (pendingStartRef.current?.sessionId === sessionId) {
|
||||||
|
pendingStartRef.current = null;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, START_TIMEOUT_MS);
|
||||||
|
|
||||||
|
pendingStartRef.current = { sessionId, resolve, timer };
|
||||||
|
const ok = sendControl({ type: "start", sessionId, version: 2 });
|
||||||
|
if (!ok) {
|
||||||
|
resolvePendingStart(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [resolvePendingStart, sendControl]);
|
||||||
|
|
||||||
|
const sendStop = useCallback(
|
||||||
|
(sessionId: string | null) => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
audioQueueRef.current = audioQueueRef.current.filter(
|
||||||
|
(packet) => packet.sessionId !== sessionId,
|
||||||
|
);
|
||||||
|
if (audioQueueRef.current.length === 0) {
|
||||||
|
useAppStore.getState().setWeakNetwork(false);
|
||||||
|
}
|
||||||
|
sendControl({ type: "stop", sessionId });
|
||||||
|
},
|
||||||
|
[sendControl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendPaste = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
const store = useAppStore.getState();
|
||||||
|
if (store.connectionStatus !== "connected") {
|
||||||
|
toast.warning("当前未连接,无法发送粘贴");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sessionId = useAppStore.getState().activeSessionId ?? undefined;
|
||||||
|
sendControl({ type: "paste", text, sessionId });
|
||||||
|
},
|
||||||
|
[sendControl],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { requestStart, sendStop, sendPaste, sendAudioFrame };
|
||||||
|
}
|
||||||
13
web/src/main.tsx
Normal file
13
web/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if (!root) throw new Error("Root element not found");
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
35
web/src/protocol.ts
Normal file
35
web/src/protocol.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export type ClientMsgType = "hello" | "start" | "stop" | "paste" | "ping";
|
||||||
|
|
||||||
|
export type ServerMsgType =
|
||||||
|
| "ready"
|
||||||
|
| "state"
|
||||||
|
| "start_ack"
|
||||||
|
| "stop_ack"
|
||||||
|
| "partial"
|
||||||
|
| "final"
|
||||||
|
| "pasted"
|
||||||
|
| "error"
|
||||||
|
| "pong";
|
||||||
|
|
||||||
|
export type SessionState = "idle" | "recording" | "stopping";
|
||||||
|
|
||||||
|
export interface ClientMsg {
|
||||||
|
type: ClientMsgType;
|
||||||
|
sessionId?: string;
|
||||||
|
seq?: number;
|
||||||
|
text?: string;
|
||||||
|
version?: number;
|
||||||
|
ts?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerMsg {
|
||||||
|
type: ServerMsgType;
|
||||||
|
state?: SessionState;
|
||||||
|
sessionId?: string;
|
||||||
|
seq?: number;
|
||||||
|
text?: string;
|
||||||
|
message?: string;
|
||||||
|
code?: string;
|
||||||
|
retryable?: boolean;
|
||||||
|
ts?: number;
|
||||||
|
}
|
||||||
88
web/src/stores/app-store.ts
Normal file
88
web/src/stores/app-store.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export interface HistoryItem {
|
||||||
|
text: string;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HISTORY_MAX = 50;
|
||||||
|
|
||||||
|
type ConnectionStatus = "connected" | "disconnected" | "connecting";
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// Connection
|
||||||
|
connectionStatus: ConnectionStatus;
|
||||||
|
weakNetwork: boolean;
|
||||||
|
// Recording
|
||||||
|
recording: boolean;
|
||||||
|
pendingStart: boolean;
|
||||||
|
stopping: boolean;
|
||||||
|
micReady: boolean;
|
||||||
|
activeSessionId: string | null;
|
||||||
|
// Preview
|
||||||
|
previewText: string;
|
||||||
|
previewActive: boolean;
|
||||||
|
// History
|
||||||
|
history: HistoryItem[];
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
||||||
|
setWeakNetwork: (weak: boolean) => void;
|
||||||
|
setRecording: (recording: boolean) => void;
|
||||||
|
setPendingStart: (pending: boolean) => void;
|
||||||
|
setStopping: (stopping: boolean) => void;
|
||||||
|
setMicReady: (ready: boolean) => void;
|
||||||
|
setActiveSessionId: (sessionId: string | null) => void;
|
||||||
|
setPreview: (text: string, isFinal: boolean) => void;
|
||||||
|
clearPreview: () => void;
|
||||||
|
addHistory: (text: string) => void;
|
||||||
|
clearHistory: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = create<AppState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
connectionStatus: "connecting",
|
||||||
|
weakNetwork: false,
|
||||||
|
recording: false,
|
||||||
|
pendingStart: false,
|
||||||
|
stopping: false,
|
||||||
|
micReady: false,
|
||||||
|
activeSessionId: null,
|
||||||
|
previewText: "",
|
||||||
|
previewActive: false,
|
||||||
|
history: [],
|
||||||
|
|
||||||
|
setConnectionStatus: (connectionStatus) => set({ connectionStatus }),
|
||||||
|
setWeakNetwork: (weakNetwork) => set({ weakNetwork }),
|
||||||
|
setRecording: (recording) => set({ recording }),
|
||||||
|
setPendingStart: (pendingStart) => set({ pendingStart }),
|
||||||
|
setStopping: (stopping) => set({ stopping }),
|
||||||
|
setMicReady: (micReady) => set({ micReady }),
|
||||||
|
setActiveSessionId: (activeSessionId) => set({ activeSessionId }),
|
||||||
|
|
||||||
|
setPreview: (text, isFinal) =>
|
||||||
|
set({
|
||||||
|
previewText: text,
|
||||||
|
previewActive: !isFinal && text.length > 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearPreview: () => set({ previewText: "", previewActive: false }),
|
||||||
|
|
||||||
|
addHistory: (text) => {
|
||||||
|
const items = [{ text, ts: Date.now() }, ...get().history].slice(
|
||||||
|
0,
|
||||||
|
HISTORY_MAX,
|
||||||
|
);
|
||||||
|
set({ history: items });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHistory: () => set({ history: [] }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "voicepaste_history",
|
||||||
|
partialize: (state) => ({ history: state.history }),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
274
web/style.css
274
web/style.css
@@ -1,274 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg: #0a0a0a;
|
|
||||||
--surface: #161616;
|
|
||||||
--surface-hover: #1e1e1e;
|
|
||||||
--border: #2a2a2a;
|
|
||||||
--text: #e8e8e8;
|
|
||||||
--text-dim: #888;
|
|
||||||
--accent: #3b82f6;
|
|
||||||
--accent-glow: rgba(59, 130, 246, 0.3);
|
|
||||||
--danger: #ef4444;
|
|
||||||
--success: #22c55e;
|
|
||||||
--radius: 12px;
|
|
||||||
--safe-top: env(safe-area-inset-top, 0px);
|
|
||||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
font-family:
|
|
||||||
-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
user-select: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: calc(16px + var(--safe-top)) 16px calc(16px + var(--safe-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 0 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status .dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--text-dim);
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.connected .dot {
|
|
||||||
background: var(--success);
|
|
||||||
}
|
|
||||||
.status.disconnected .dot {
|
|
||||||
background: var(--danger);
|
|
||||||
}
|
|
||||||
.status.connecting .dot {
|
|
||||||
background: var(--accent);
|
|
||||||
animation: pulse 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview */
|
|
||||||
#preview-section {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-box {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 16px;
|
|
||||||
min-height: 80px;
|
|
||||||
max-height: 160px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-box.active {
|
|
||||||
border-color: var(--accent);
|
|
||||||
box-shadow: 0 0 0 1px var(--accent-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
#preview-text {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#preview-text.placeholder {
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mic Button */
|
|
||||||
#mic-section {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 24px 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mic-btn {
|
|
||||||
width: 88px;
|
|
||||||
height: 88px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-dim);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mic-btn:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mic-btn:not(:disabled):active,
|
|
||||||
#mic-btn.recording {
|
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
transform: scale(1.08);
|
|
||||||
box-shadow: 0 0 24px var(--accent-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
#mic-btn.recording {
|
|
||||||
animation: mic-pulse 1s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mic-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 24px var(--accent-glow);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow:
|
|
||||||
0 0 48px var(--accent-glow),
|
|
||||||
0 0 80px rgba(59, 130, 246, 0.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* History */
|
|
||||||
#history-section {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.history-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.history-header h2 {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-dim);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
.text-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.text-btn:active {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
#history-list {
|
|
||||||
list-style: none;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
#history-list li {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 12px 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
#history-list li:active {
|
|
||||||
background: var(--surface-hover);
|
|
||||||
}
|
|
||||||
#history-list li .hist-text {
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
#history-list li .hist-time {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
#history-empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: 32px 0;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
/* Scrollbar */
|
|
||||||
#history-list::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
#history-list::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
#history-list::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
@@ -11,5 +12,5 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "vite-env.d.ts"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "vite-env.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,50 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react({ babel: { plugins: ["babel-plugin-react-compiler"] } }),
|
||||||
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
|
||||||
|
// Don't cache API/WebSocket requests
|
||||||
|
navigateFallbackDenylist: [/^\/api/, /^\/ws/],
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: "VoicePaste",
|
||||||
|
short_name: "VoicePaste",
|
||||||
|
description: "语音转文字,一键粘贴到电脑剪贴板",
|
||||||
|
theme_color: "#08080d",
|
||||||
|
background_color: "#08080d",
|
||||||
|
display: "standalone",
|
||||||
|
orientation: "portrait",
|
||||||
|
scope: "/",
|
||||||
|
start_url: "/",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "pwa-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "pwa-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "pwa-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
root: ".",
|
root: ".",
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
|
|||||||
Reference in New Issue
Block a user