From 24b07a7bb2aea74154270577a4028aa7fc322e52 Mon Sep 17 00:00:00 2001 From: shin-fukasawa1 Date: Fri, 17 Apr 2026 16:11:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=AA=E3=82=A2=E3=83=AB=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=A0=E8=AD=B0=E4=BA=8B=E9=8C=B2=E3=82=B7=E3=82=B9?= =?UTF-8?q?=E3=83=86=E3=83=A0=E5=88=9D=E6=9C=9F=E3=83=AA=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .cursorrules | 1 + .env.example | 10 + .gitignore | 5 + README.md | 214 +++ config.yaml | 25 + index.html | 12 + justfile | 21 + package.json | 43 + pnpm-lock.yaml | 2124 ++++++++++++++++++++++++++++ postcss.config.js | 3 + server/config.ts | 155 ++ server/constants.ts | 9 + server/deepgram-relay.ts | 278 ++++ server/env.ts | 133 ++ server/index.ts | 195 +++ server/logger.ts | 10 + server/preflight.ts | 114 ++ server/schemas.ts | 15 + server/task-extractor.ts | 190 +++ server/transcript-writer.ts | 72 + server/types.ts | 17 + src/App.tsx | 9 + src/components/recording-panel.tsx | 146 ++ src/components/ui/badge.tsx | 27 + src/components/ui/button.tsx | 30 + src/hooks/use-recorder.ts | 296 ++++ src/index.css | 37 + src/lib/utils.ts | 6 + src/main.tsx | 10 + src/vite-env.d.ts | 1 + tsconfig.json | 22 + tsconfig.server.json | 17 + vite.config.ts | 7 + 33 files changed, 4254 insertions(+) create mode 100644 .cursorrules create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.yaml create mode 100644 index.html create mode 100644 justfile create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 server/config.ts create mode 100644 server/constants.ts create mode 100644 server/deepgram-relay.ts create mode 100644 server/env.ts create mode 100644 server/index.ts create mode 100644 server/logger.ts create mode 100644 server/preflight.ts create mode 100644 server/schemas.ts create mode 100644 server/task-extractor.ts create mode 100644 server/transcript-writer.ts create mode 100644 server/types.ts create mode 100644 src/App.tsx create mode 100644 src/components/recording-panel.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/hooks/use-recorder.ts create mode 100644 src/index.css create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.server.json create mode 100644 vite.config.ts diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..3db4167 --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +常に日本語で回答してください。 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5225dda --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# ===== 必須 ===== +# 1P: ADS 議事録システム / Deepgram API Key +DEEPGRAM_API_KEY={{ op://AI-Driven School/aymxccrse4mmlhunqej4ifjjme/password }} +# 1P: ADS 議事録システム / Google AI API Key +GOOGLE_GENERATIVE_AI_API_KEY={{ op://AI-Driven School/gj6ah5wib6to3pxydvx2xbthve/password }} + +# ===== オプション ===== +# PORT=3001 +# +# 会議設定(参加者・キーターム・置換ルール)は config.yaml で管理します diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..794b761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +output/ +dist/ +.env +pnpm-config.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..2969f74 --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# リアルタイム議事録システム + +会議音声をリアルタイムで文字起こしし、タスクを自動抽出するツールです。 + +## 何ができるか + +マイクで拾った会議の音声を、裏側でリアルタイムに文字起こしします。さらに 30 秒ごとに会話内容を AI が読み取り、「誰が/いつまでに/何をやるか」をタスクとして自動で抜き出します。 + +``` +マイク → 音声認識(Deepgram) → 文字起こしファイル(transcript.md) + ↓ + AI(Gemini) → タスク一覧ファイル(tasks.md) +``` + +会議が終わると、手元に 2 つの Markdown ファイルが残ります。 + +- **transcript.md** — 会議の全文記録(タイムスタンプ付き) +- **tasks.md** — 抽出されたタスクの一覧表(担当者・期限・根拠つき) + +## 前提条件 + +以下のツールが必要です。 + +| ツール | バージョン | インストール方法 | +|--------|-----------|-----------------| +| Node.js | 20 以上 | https://nodejs.org/ | +| pnpm | 最新 | `npm install -g pnpm` | +| just | 最新 | `brew install just`(macOS) | + +## セットアップ + +### 1. API キーの取得 + +2つの API キーが必要です。 + +**Deepgram**(音声認識) + +1. https://console.deepgram.com/ にアクセス +2. アカウントを作成(または既存アカウントでログイン) +3. 「API Keys」から新しいキーを作成 +4. キーをコピー + +**Google AI**(タスク抽出) + +1. https://aistudio.google.com/apikey にアクセス +2. Google アカウントでログイン +3. 「Create API key」でキーを作成 +4. キーをコピー + +### 2. .env ファイルの作成 + +```bash +cp .env.example .env +``` + +テキストエディタで `.env` を開き、取得した API キーを貼り付けてください。 + +``` +DEEPGRAM_API_KEY=ここにDeepgramのキーを貼り付け +GOOGLE_GENERATIVE_AI_API_KEY=ここにGoogle AIのキーを貼り付け +``` + +### 3. 会議設定(config.yaml) + +`config.yaml` を開いて、会議に合わせて編集します。設定しなくても動きますが、設定すると精度が上がります。 + +**参加者名** + +会議の参加者名を登録すると、音声認識で名前が正しく聞き取られやすくなります。タスク抽出でも、担当者名の表記ミスを防げます。 + +```yaml +participants: + - 田中 + - 鈴木 + - 佐藤 +``` + +**キーターム(専門用語)** + +会議で出てくる専門用語・プロジェクト名・ブランド名を登録すると、音声認識の精度が上がります。20〜50 語が目安です。 + +```yaml +keyterms: + - AI-Driven School + - Deepgram + - Linear + - Slack +``` + +**置換ルール(任意)** + +音声認識でよくある誤変換パターンを自動修正できます。キータームで解決しない場合の補助手段です。 + +```yaml +replacements: + - find: 深グラム + replace: Deepgram +``` + +## 使い方 + +### 立ち上げ方 + +1. Cursor でこのプロジェクトフォルダを開く +2. **Cmd + J** でターミナルを開く +3. 以下を入力して Enter + +```bash +just start +``` + +初回は必要なパッケージの自動インストールが走るため、数十秒ほどかかります。ターミナルに起動完了のメッセージが出たら準備完了です。 + +4. ブラウザで http://localhost:5173 を開く + +> `just` はよく使うコマンドに名前を付けてまとめたものです。中身を知らなくても `just start` だけ覚えれば OK です。 +> +> ターミナル操作に不安がある場合は、Cursor の AI に「このプロジェクトを立ち上げて」と頼んでも大丈夫です。 + +### 録音する + +1. ブラウザで「**録音を開始**」ボタンを押す + - 初回はマイクの使用許可を求められるので「許可」を選んでください +2. 録音中は画面に最新の文字起こしが表示されます。裏では 30 秒ごとにタスクも自動抽出されています +3. 会議が終わったら「**録音を停止**」ボタンを押す + +### 停止 + +- ターミナルで **Ctrl+C** を押す +- または別のターミナルで `just stop` を実行 + +### 出力ファイル + +`output/` フォルダに、録音ごとに 2 つのファイルが生成されます。 + +``` +output/ + meeting-2026-04-17T14-00-00-transcript.md + meeting-2026-04-17T14-00-00-tasks.md +``` + +**transcript.md の例** + +```markdown +# 会議メモ 2026/04/17 14:00 + +## 参加者 +田中、鈴木、佐藤 + +--- + +**[14:00:05]** 今日は来週のリリースについて話しましょう + +**[14:00:12]** デザインの最終確認がまだ終わっていないんですが + +**[14:00:20]** 佐藤さん、明日までにレビューお願いできますか +``` + +**tasks.md の例** + +```markdown +# 抽出タスク + +| # | タスク | 担当 | 期限 | 根拠 | +|---|--------|------|------|------| +| 1 | デザインの最終確認レビュー | 佐藤 | 明日 | 佐藤さん、明日までにレビューお願いできますか | +``` + +議事録としてチームに共有したり、タスクをスプレッドシートや Linear に転記して使えます。 + +## トラブルシューティング + +### 「.env ファイルが見つかりません」と表示される + +上記の「.env ファイルの作成」の手順に従ってください。 + +### 「APIキーが無効です」と表示される + +- API キーが正しくコピーされているか確認してください(前後にスペースが入っていないか) +- Deepgram: https://console.deepgram.com/ でキーが有効か確認 +- Google AI: https://aistudio.google.com/apikey でキーが有効か確認 + +### 「ポート 3001 が使用中です」と表示される + +前回のサーバーが残っている可能性があります。 + +```bash +just stop +``` + +を実行してから、再度 `just start` を試してください。 + +### マイクが使えない / ブロックされている + +Chrome の場合: アドレスバーの鍵アイコン →「サイトの設定」→ マイク →「許可」に変更し、ページを再読み込みしてください。 + +### 「サーバーとの接続に失敗しました」と表示される + +サーバーが起動していない可能性があります。ターミナルでエラーが出ていないか確認し、`just start` で再起動してください。 + +### 文字起こしの精度が低い + +- `config.yaml` のキータームに、会議で出てくる専門用語を追加してください +- マイクに近い位置で話すと精度が上がります +- 複数人が同時に話すと認識精度が下がります + +### タスクが抽出されない + +- 会話が短すぎると(2 文以下)タスクは抽出されません +- Google AI の無料枠を超えると一時的にタスク抽出が停止します。しばらく待つか、API キーのプランを確認してください + +### 推奨ブラウザ + +**Chrome** を推奨します。Safari や Firefox ではマイクの挙動が異なる場合があります。 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..1dac9e2 --- /dev/null +++ b/config.yaml @@ -0,0 +1,25 @@ +# 会議設定 +# このファイルを編集して、音声認識の精度をチューニングできます + +# 参加者名(タスク抽出の担当者候補として使用) +# 参加者名はキータームにも自動追加され、名前の認識精度が上がります +participants: + - 田中 + - 鈴木 + - 佐藤 + +# 音声認識のキーターム +# 会議で出てくる専門用語・プロジェクト名・ブランド名を指定すると認識精度が上がります +# 上限: 全キータームで合計500トークン(20〜50語が目安) +keyterms: + - AI-Driven School + - Deepgram + - Linear + - Slack + +# 認識後の置換ルール(任意) +# 既知の誤変換パターンを修正します +# keyterms で解決しない場合の補助手段です +replacements: + - find: 深グラム + replace: Deepgram diff --git a/index.html b/index.html new file mode 100644 index 0000000..f5a82c6 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + リアルタイム議事録 + + +
+ + + diff --git a/justfile b/justfile new file mode 100644 index 0000000..b9f9340 --- /dev/null +++ b/justfile @@ -0,0 +1,21 @@ +# リアルタイム議事録システム + +# サーバー起動 +start *args: + #!/usr/bin/env bash + command -v node >/dev/null 2>&1 || { echo "Node.js がインストールされていません。https://nodejs.org/ からインストールしてください"; exit 1; } + command -v pnpm >/dev/null 2>&1 || { echo "pnpm がインストールされていません。npm install -g pnpm を実行してください"; exit 1; } + [ -d node_modules ] || pnpm install + pnpm tsx server/preflight.ts + pnpm dev {{args}} + +# サーバー停止 +stop: + #!/usr/bin/env bash + pids=$(lsof -ti :3001,:5173 2>/dev/null) + [[ -n "$pids" ]] && kill $pids 2>/dev/null || true + +# サーバー再起動 +restart *args: + just stop + just start {{args}} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f98234 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "realtime-meeting-minutes", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently --kill-others-on-fail \"vite\" \"tsx watch server/index.ts\"", + "dev:client": "vite", + "dev:server": "tsx watch server/index.ts", + "build": "tsc && vite build" + }, + "packageManager": "pnpm@10.24.0", + "dependencies": { + "@ai-sdk/google": "^3.0.60", + "@deepgram/sdk": "^5.0.0", + "ai": "^6.0.154", + "clsx": "^2.1.1", + "dotenv": "^17.4.1", + "express": "^5.2.1", + "lucide-react": "^1.8.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "tailwind-merge": "^3.5.0", + "ws": "^8.20.0", + "yaml": "^2.8.3", + "zod": "^4.3.6" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@types/express": "^5.0.6", + "@types/node": "^25.5.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^9.2.1", + "postcss": "^8.5.9", + "tailwindcss": "^4.2.2", + "tsx": "^4.21.0", + "typescript": "^6.0.2", + "vite": "^8.0.8" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..999531d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2124 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/google': + specifier: ^3.0.60 + version: 3.0.60(zod@4.3.6) + '@deepgram/sdk': + specifier: ^5.0.0 + version: 5.0.0 + ai: + specifier: ^6.0.154 + version: 6.0.154(zod@4.3.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dotenv: + specifier: ^17.4.1 + version: 17.4.1 + express: + specifier: ^5.2.1 + version: 5.2.1 + lucide-react: + specifier: ^1.8.0 + version: 1.8.0(react@19.2.5) + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + ws: + specifier: ^8.20.0 + version: 8.20.0 + yaml: + specifier: ^2.8.3 + version: 2.8.3 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^25.5.2 + version: 25.5.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + postcss: + specifier: ^8.5.9 + version: 8.5.9 + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + +packages: + + '@ai-sdk/gateway@3.0.94': + resolution: {integrity: sha512-uDDwLZhCkvC89crVS3S90D5L7AcVN8WriGuYVNYgVAaVcvy3Mthy3R9ICfzG75BObhz6pm2FWnhxDfNRK+t69Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.60': + resolution: {integrity: sha512-ye/hG0LeO24VmjLbfgkFZV8V8k/l4nVBODutpJQkFPyUiGOCbFtFUTgxSeC7+njrk5+HhgyHrzJay4zmhwMH+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.23': + resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + + '@deepgram/sdk@5.0.0': + resolution: {integrity: sha512-x1wMiOgDGqcLEaQpQBQLTtk5mLbXbYgcBEpp7cfJIyEtqdIGgijCZH+a/esiVp+xIcTYYroTxG47RVppZOHbWw==} + engines: {node: '>=18.0.0'} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ai@6.0.154: + resolution: {integrity: sha512-HfKJKCTJsDZxqrIUDSVnBQ7DpQlx5WI4ExqtLd7Bl70epLmvkpc/HYMzU1hP9W+g9VEAcvZo4fbMqc3v5D+9gQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lucide-react@1.8.0: + resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@ai-sdk/gateway@3.0.94(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + + '@ai-sdk/google@3.0.60(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.23(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + + '@deepgram/sdk@5.0.0': + dependencies: + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@opentelemetry/api@1.9.0': {} + + '@oxc-project/types@0.124.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.5.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.5.2 + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.5.2 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/node@25.5.2': + dependencies: + undici-types: 7.18.2 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.5.2 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.5.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.5.2 + + '@vercel/oidc@3.1.0': {} + + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ai@6.0.154(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.94(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.4.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-promise@4.0.0: {} + + jiti@2.6.1: {} + + json-schema@0.4.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lucide-react@1.8.0(react@19.2.5): + dependencies: + react: 19.2.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.4.2: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react@19.2.5: {} + + require-directory@2.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@6.0.2: {} + + undici-types@7.18.2: {} + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + vite@8.0.8(@types/node@25.5.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.5.2 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + yaml: 2.8.3 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.20.0: {} + + y18n@5.0.8: {} + + yaml@2.8.3: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + zod@4.3.6: {} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..3e0d24c --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,3 @@ +export default { + plugins: {}, +}; diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..453ee1b --- /dev/null +++ b/server/config.ts @@ -0,0 +1,155 @@ +import fs from "node:fs"; +import { parse as parseYaml } from "yaml"; +import { getEnv } from "./env.js"; +import { logger } from "./logger.js"; + +export interface Replacement { + find: string; + replace: string; +} + +export interface Config { + server: { + port: number; + }; + deepgram: { + model: string; + language: string; + diarize: boolean; + interimResults: boolean; + endpointing: number | false; + utteranceEndMs: number; + smartFormat: boolean; + punctuate: boolean; + keyterms: string[]; + replacements: Replacement[]; + }; + keepAlive: { + intervalMs: number; + idleThresholdMs: number; + }; + taskExtraction: { + model: string; + intervalMs: number; + llmMaxRetries: number; + maxConsecutiveFailures: number; + maxBackoffMs: number; + }; + reconnect: { + maxAttempts: number; + maxBufferBytes: number; + }; + output: { + dir: string; + }; + participants: string[]; +} + +// --- config.yaml 読み込み --- + +const CONFIG_YAML_PATH = "config.yaml"; + +interface MeetingConfig { + participants: string[]; + keyterms: string[]; + replacements: Replacement[]; +} + +const DEFAULT_MEETING_CONFIG: MeetingConfig = { + participants: [], + keyterms: [], + replacements: [], +}; + +function filterStrings(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((v): v is string => typeof v === "string") + : []; +} + +function isReplacement(v: unknown): v is Replacement { + if (typeof v !== "object" || v === null) return false; + const r = v as Record; + return typeof r.find === "string" && typeof r.replace === "string"; +} + +function loadMeetingConfig(): MeetingConfig { + if (!fs.existsSync(CONFIG_YAML_PATH)) { + logger.info("config.yaml が見つかりません。デフォルト設定で起動します"); + return DEFAULT_MEETING_CONFIG; + } + + try { + const raw = fs.readFileSync(CONFIG_YAML_PATH, "utf-8"); + const parsed = parseYaml(raw) as Record; + + const config: MeetingConfig = { + participants: filterStrings(parsed.participants), + keyterms: filterStrings(parsed.keyterms), + replacements: Array.isArray(parsed.replacements) + ? parsed.replacements.filter(isReplacement) + : [], + }; + + logger.info( + `config.yaml: 参加者 ${config.participants.length} 名, キーターム ${config.keyterms.length} 件, 置換ルール ${config.replacements.length} 件`, + ); + + return config; + } catch (e) { + logger.warn( + "config.yaml の読み込みに失敗しました。デフォルト設定で起動します", + e, + ); + return DEFAULT_MEETING_CONFIG; + } +} + +// プロセスライフタイムで1回だけ初期化される設定値(録音セッション状態とは無関係) +let _config: Config | null = null; + +export function getConfig(): Config { + if (!_config) { + const env = getEnv(); + const meeting = loadMeetingConfig(); + _config = { + server: { + port: env.PORT, + }, + deepgram: { + model: "nova-3", + language: "ja", + diarize: false, + interimResults: true, + endpointing: 500, + utteranceEndMs: 1000, + smartFormat: true, + punctuate: true, + keyterms: [ + ...new Set([...meeting.participants, ...meeting.keyterms]), + ], + replacements: meeting.replacements, + }, + keepAlive: { + intervalMs: 3_000, + idleThresholdMs: 5_000, + }, + taskExtraction: { + model: "gemini-3-flash-preview", + intervalMs: 30_000, + llmMaxRetries: 0, + maxConsecutiveFailures: 3, + maxBackoffMs: 180_000, + }, + reconnect: { + maxAttempts: 3, + maxBufferBytes: 2 * 1024 * 1024, + }, + output: { + dir: "output", + }, + participants: meeting.participants, + }; + } + return _config; +} diff --git a/server/constants.ts b/server/constants.ts new file mode 100644 index 0000000..e73973d --- /dev/null +++ b/server/constants.ts @@ -0,0 +1,9 @@ +// preflight.ts と env.ts の両方から参照される定数。 +// 外部ライブラリに依存しないため、preflight.ts を単独実行しても安全。 + +export const REQUIRED_KEYS = [ + "DEEPGRAM_API_KEY", + "GOOGLE_GENERATIVE_AI_API_KEY", +] as const; + +export const DEFAULT_PORT = 3001; diff --git a/server/deepgram-relay.ts b/server/deepgram-relay.ts new file mode 100644 index 0000000..a6ff2c3 --- /dev/null +++ b/server/deepgram-relay.ts @@ -0,0 +1,278 @@ +import { DeepgramClient } from "@deepgram/sdk"; +import type { WebSocket } from "ws"; +import { getConfig, type Config, type Replacement } from "./config.js"; +import { TranscriptWriter } from "./transcript-writer.js"; +import type { ServerMessage } from "./types.js"; +import { logger } from "./logger.js"; + +type V1Socket = Awaited< + ReturnType["listen"]["v1"]["connect"]> +>; + +function formatReplacement(r: Replacement): string { + return `${r.find}:${r.replace}`; +} + +export class DeepgramRelay { + readonly timestamp: string; + readonly llmInputBuffer: string[] = []; + readonly outputPath: string; + + private readonly config: Config; + private readonly writer: TranscriptWriter; + private readonly meetingStartTime: Date; + private readonly connectArgs: Parameters["listen"]["v1"]["connect"]>[0]; + + private connection: V1Socket | null = null; + private keepAliveTimer: ReturnType | null = null; + private lastDataSentAt = Date.now(); + private timestampOffsetMs = 0; + + private reconnecting = false; + private readonly audioBuffer: Buffer[] = []; + private audioBufferBytes = 0; + + constructor( + private readonly apiKey: string, + private readonly browserWs: WebSocket, + ) { + this.config = getConfig(); + this.meetingStartTime = new Date(); + this.timestamp = this.meetingStartTime + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); + this.writer = new TranscriptWriter(this.timestamp); + this.outputPath = this.writer.getOutputPath(); + + this.connectArgs = { + model: this.config.deepgram.model, + language: this.config.deepgram.language, + encoding: "linear16", + sample_rate: "16000", + channels: "1", + ...(this.config.deepgram.diarize ? { diarize: "true" } : {}), + interim_results: String(this.config.deepgram.interimResults), + endpointing: String(this.config.deepgram.endpointing), + utterance_end_ms: String(this.config.deepgram.utteranceEndMs), + smart_format: String(this.config.deepgram.smartFormat), + punctuate: String(this.config.deepgram.punctuate), + Authorization: this.apiKey, + queryParams: this.buildQueryParams(), + }; + } + + async start(participants: string[]): Promise { + await this.writer.init(participants); + this.connection = await this.openConnection(); + this.startKeepAlive(); + } + + sendAudio(chunk: Buffer): void { + if (chunk.byteLength === 0) return; + this.lastDataSentAt = Date.now(); + + if (this.reconnecting) { + this.bufferChunk(chunk); + return; + } + + try { + this.connection?.sendMedia(chunk); + } catch { + logger.warn("音声送信失敗 — 再接続を開始"); + this.bufferChunk(chunk); + void this.reconnect(); + } + } + + async stop(): Promise { + this.sendToClient({ type: "stopping" }); + + try { this.connection?.sendFinalize({ type: "Finalize" }); } catch {} + await this.wait(5000); + try { this.connection?.sendCloseStream({ type: "CloseStream" }); } catch {} + + this.clearKeepAlive(); + await this.writer.flush(); + + try { this.connection?.close(); } catch {} + this.connection = null; + } + + private buildQueryParams(): Record | undefined { + const params: Record = {}; + const { keyterms, replacements } = this.config.deepgram; + + if (keyterms.length > 0) { + params.keyterm = keyterms; + logger.info(`Deepgram keyterms: ${keyterms.join(", ")}`); + } + + if (replacements.length > 0) { + params.replace = replacements.map(formatReplacement); + logger.info( + `Deepgram replace: ${replacements.map(formatReplacement).join(", ")}`, + ); + } + + return Object.keys(params).length > 0 ? params : undefined; + } + + // --- Private: Connection lifecycle --- + + private async openConnection(): Promise { + const client = new DeepgramClient({ apiKey: this.apiKey }); + const conn = await client.listen.v1.connect(this.connectArgs); + + conn.on("open", () => { + logger.info("Deepgram 接続確立"); + if (!this.reconnecting) { + this.sendToClient({ type: "ready", outputPath: this.outputPath }); + } + }); + + conn.on("message", (data) => this.handleTranscriptMessage(data)); + + conn.on("error", (error) => { + logger.error("Deepgram エラー", error); + this.sendToClient({ type: "error", code: "DEEPGRAM_ERROR", message: error.message }); + }); + + conn.on("close", () => { + logger.info("Deepgram 接続終了"); + }); + + conn.connect(); + await conn.waitForOpen(); + return conn; + } + + private handleTranscriptMessage(data: { type: string }): void { + if (data.type !== "Results") return; + + const result = data as unknown as { + is_final?: boolean; + speech_final?: boolean; + start: number; + channel: { alternatives: { transcript: string; words: { speaker?: number }[] }[] }; + }; + + const alt = result.channel.alternatives[0]; + if (!alt) return; + + const { transcript } = alt; + const isFinal = result.is_final ?? false; + const speechFinal = result.speech_final ?? false; + + this.sendToClient({ type: "transcript", text: transcript, isFinal }); + + if (!isFinal || !transcript.trim()) return; + + const ts = this.formatTimestamp(result.start + this.timestampOffsetMs / 1000); + this.writer.appendUtterance(transcript, ts, speechFinal); + this.llmInputBuffer.push(transcript); + } + + // --- Private: Reconnection --- + + private async reconnect(): Promise { + if (this.reconnecting) return; + this.reconnecting = true; + this.sendToClient({ type: "reconnecting" }); + this.clearKeepAlive(); + + const elapsedMs = Date.now() - this.meetingStartTime.getTime(); + + for (let attempt = 1; attempt <= this.config.reconnect.maxAttempts; attempt++) { + const backoff = Math.pow(2, attempt - 1) * 1000; + logger.info(`Deepgram 再接続 試行 ${attempt}/${this.config.reconnect.maxAttempts} (${backoff}ms後)`); + await this.wait(backoff); + + try { + this.connection = await this.openConnection(); + this.timestampOffsetMs = elapsedMs; + this.drainAudioBuffer(); + this.startKeepAlive(); + this.reconnecting = false; + this.sendToClient({ type: "reconnected" }); + logger.info("Deepgram 再接続成功"); + return; + } catch (e) { + logger.error(`再接続試行 ${attempt} 失敗`, e); + } + } + + this.reconnecting = false; + this.sendToClient({ + type: "error", + code: "DEEPGRAM_RECONNECT_FAILED", + message: `Deepgram への再接続に ${this.config.reconnect.maxAttempts} 回失敗しました`, + }); + } + + // --- Private: KeepAlive --- + + private startKeepAlive(): void { + this.clearKeepAlive(); + this.keepAliveTimer = setInterval(() => { + if (Date.now() - this.lastDataSentAt > this.config.keepAlive.idleThresholdMs) { + try { this.connection?.sendKeepAlive({ type: "KeepAlive" }); } catch { + logger.warn("KeepAlive 送信失敗"); + } + } + }, this.config.keepAlive.intervalMs); + } + + private clearKeepAlive(): void { + if (this.keepAliveTimer) { + clearInterval(this.keepAliveTimer); + this.keepAliveTimer = null; + } + } + + // --- Private: Audio buffer (reconnection) --- + + private bufferChunk(chunk: Buffer): void { + while ( + this.audioBufferBytes + chunk.byteLength > this.config.reconnect.maxBufferBytes && + this.audioBuffer.length > 0 + ) { + const dropped = this.audioBuffer.shift()!; + this.audioBufferBytes -= dropped.byteLength; + logger.warn("再接続バッファ上限超過: 古いチャンクを破棄"); + } + this.audioBuffer.push(chunk); + this.audioBufferBytes += chunk.byteLength; + } + + private drainAudioBuffer(): void { + const chunks = this.audioBuffer.splice(0); + this.audioBufferBytes = 0; + for (const chunk of chunks) { + try { this.connection?.sendMedia(chunk); } catch { break; } + } + } + + // --- Private: Utilities --- + + private sendToClient(msg: ServerMessage): void { + if (this.browserWs.readyState === this.browserWs.OPEN) { + this.browserWs.send(JSON.stringify(msg)); + } + } + + private formatTimestamp(startSeconds: number): string { + const d = new Date(this.meetingStartTime.getTime() + startSeconds * 1000); + return d.toLocaleTimeString("ja-JP", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + } + + private wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/server/env.ts b/server/env.ts new file mode 100644 index 0000000..6bc1df8 --- /dev/null +++ b/server/env.ts @@ -0,0 +1,133 @@ +import { config as dotenvConfig } from "dotenv"; +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import { z } from "zod"; +import { REQUIRED_KEYS } from "./constants.js"; +import { logger } from "./logger.js"; + +const envSchema = z.object({ + DEEPGRAM_API_KEY: z.string().min(1), + GOOGLE_GENERATIVE_AI_API_KEY: z.string().min(1), + PORT: z + .string() + .default("3001") + .transform(Number) + .pipe(z.number().int().min(1).max(65535)), +}); + +export type Env = z.infer; + +export class EnvError extends Error { + constructor(message: string) { + super(message); + this.name = "EnvError"; + } +} + +// プロセスライフタイムで1回だけ初期化される設定値(録音セッション状態とは無関係) +let _env: Env | null = null; + +export function getEnv(): Env { + if (!_env) _env = loadEnv(); + return _env; +} + +function loadEnv(): Env { + dotenvConfig(); + + const first = envSchema.safeParse(process.env); + if (first.success) return first.data; + + if (!fs.existsSync(".env") && tryOpInject()) { + dotenvConfig({ override: true }); + const retry = envSchema.safeParse(process.env); + if (retry.success) { + logger.info("1Password CLI で .env を自動生成しました"); + return retry.data; + } + } + + const { fieldErrors } = z.flattenError(first.error); + throw new EnvError(formatDiagnostics(fieldErrors)); +} + +function tryOpInject(): boolean { + try { + execSync("op --version", { stdio: "ignore", timeout: 3_000 }); + } catch { + return false; + } + try { + logger.info("1Password CLI で .env を生成中..."); + execSync("op inject -i .env.example -o .env", { + stdio: "inherit", + timeout: 30_000, + }); + return true; + } catch { + logger.warn("1Password CLI での .env 生成に失敗しました"); + return false; + } +} + +function hasOpCli(): boolean { + try { + execSync("op --version", { stdio: "ignore", timeout: 3_000 }); + return true; + } catch { + return false; + } +} + +function formatDiagnostics( + fieldErrors: Record, +): string { + const issues = Object.keys(fieldErrors) + .map((key) => { + const val = process.env[key]; + const status = + val === undefined ? "未設定" : val === "" ? "空です" : "値が不正です"; + return ` ${key}: ${status}`; + }) + .join("\n"); + + const opAvailable = hasOpCli(); + + const lines = [ + "", + "============================================================", + " 環境変数が設定されていません", + "============================================================", + "", + " 不足している変数:", + issues, + "", + " -- 解決方法 -----------------------------------------------", + "", + ]; + + if (opAvailable) { + lines.push( + " [方法1] 1Password CLI で自動生成(推奨)", + " $ op inject -i .env.example -o .env", + "", + ); + } + + lines.push( + opAvailable + ? " [方法2] 手動で .env ファイルを作成" + : " .env ファイルを手動で作成してください", + "", + ...REQUIRED_KEYS.map((key) => ` ${key}=your-key-here`), + "", + " API キーの取得先:", + " Deepgram : https://console.deepgram.com/", + " Google AI : https://aistudio.google.com/apikey", + "", + "============================================================", + "", + ); + + return lines.join("\n"); +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..aedb6d3 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,195 @@ +import http from "node:http"; +import express from "express"; +import { WebSocketServer, type WebSocket } from "ws"; +import { DeepgramClient } from "@deepgram/sdk"; +import { getEnv, EnvError } from "./env.js"; +import { logger } from "./logger.js"; +import { getConfig } from "./config.js"; +import { DeepgramRelay } from "./deepgram-relay.js"; +import { taskExtractionLoop } from "./task-extractor.js"; +import type { ClientMessage, ServerMessage, TaskStatus } from "./types.js"; + +async function validateDeepgramKey(apiKey: string): Promise { + try { + const client = new DeepgramClient({ apiKey }); + await client.manage.v1.projects.list(); + logger.info("Deepgram: OK"); + } catch (e) { + if (hasHttpStatus(e, 401, 403)) { + logger.error("Deepgram: APIキーが無効です。.env を確認してください"); + process.exit(1); + } + logger.warn("Deepgram: 検証リクエストに失敗しました(起動は続行)", e); + } +} + +async function validateGoogleAiKey(apiKey: string): Promise { + const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}&pageSize=1`; + try { + const res = await fetch(url); + if (res.status === 401 || res.status === 403) { + logger.error("Google AI: APIキーが無効です。.env を確認してください"); + process.exit(1); + } + if (!res.ok) { + logger.warn(`Google AI: 検証リクエストが ${res.status} を返しました(起動は続行)`); + return; + } + logger.info("Google AI: OK"); + } catch (e) { + logger.warn("Google AI: 検証リクエストに失敗しました(起動は続行)", e); + } +} + +function hasHttpStatus(e: unknown, ...codes: number[]): boolean { + if (!(e instanceof Error)) return false; + const record = e as Record; + const status = record["statusCode"] ?? record["status"]; + if (typeof status === "number") return codes.includes(status); + const msg = e.message.toLowerCase(); + return codes.some( + (code) => msg.includes(String(code)) || msg.includes(HTTP_STATUS_NAMES[code] ?? ""), + ); +} + +const HTTP_STATUS_NAMES: Record = { + 401: "unauthorized", + 403: "forbidden", +}; + +function sendToWs(ws: WebSocket, msg: ServerMessage): void { + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify(msg)); + } +} + +function tryParseClientMessage(text: string): ClientMessage | null { + try { + const parsed = JSON.parse(text) as Record; + if (parsed.type === "stop") return { type: "stop" }; + return null; + } catch { + return null; + } +} + +async function main() { + const env = getEnv(); + const config = getConfig(); + + const participants = config.participants; + logger.info(`参加者: ${participants.length > 0 ? participants.join(", ") : "(未指定)"}`); + + await validateDeepgramKey(env.DEEPGRAM_API_KEY); + await validateGoogleAiKey(env.GOOGLE_GENERATIVE_AI_API_KEY); + + const app = express(); + const server = http.createServer(app); + const wss = new WebSocketServer({ noServer: true }); + + server.on("upgrade", (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + }); + + wss.on("connection", (ws: WebSocket) => { + void handleSession(ws, env.DEEPGRAM_API_KEY, participants); + }); + + const port = config.server.port; + server.listen(port, () => { + logger.info(`サーバー起動: http://localhost:${port}`); + }); +} + +async function handleSession( + ws: WebSocket, + apiKey: string, + participants: string[], +): Promise { + logger.info("ブラウザ WebSocket 接続"); + + const config = getConfig(); + const relay = new DeepgramRelay(apiKey, ws); + + let stopping = false; + let lastTaskStatus: TaskStatus = { taskCount: 0, failing: false }; + let taskAbortController: AbortController | null = null; + let taskExtractionPromise: Promise | null = null; + + try { + await relay.start(participants); + } catch (e) { + logger.error("Deepgram リレー起動失敗", e); + sendToWs(ws, { + type: "error", + code: "DEEPGRAM_CONNECT_FAILED", + message: "Deepgram への接続に失敗しました", + }); + ws.close(); + return; + } + + taskAbortController = new AbortController(); + taskExtractionPromise = taskExtractionLoop( + { + participants, + outputDir: config.output.dir, + timestamp: relay.timestamp, + llmInputBuffer: relay.llmInputBuffer, + onStatusChange: (status) => { + lastTaskStatus = status; + sendToWs(ws, { type: "task_status", ...status }); + }, + }, + taskAbortController.signal, + ); + + async function gracefulStop(): Promise { + await relay.stop(); + + sendToWs(ws, { type: "stopped", ...lastTaskStatus }); + ws.close(); + + taskAbortController?.abort(); + if (taskExtractionPromise) { + const result = await taskExtractionPromise.catch((): null => null); + if (result && result.taskCount > 0) { + logger.info(`タスク抽出完了: ${result.taskCount} 件`); + } + } + } + + ws.on("message", (data: Buffer | string, isBinary: boolean) => { + if (isBinary) { + relay.sendAudio(data as Buffer); + return; + } + + const msg = tryParseClientMessage(String(data)); + if (msg?.type === "stop" && !stopping) { + stopping = true; + logger.info("停止要求を受信"); + void gracefulStop(); + } + }); + + ws.on("close", () => { + logger.info("ブラウザ WebSocket 切断"); + if (!stopping) { + taskAbortController?.abort(); + taskExtractionPromise?.catch(() => {}); + void relay.stop().catch(() => {}); + } + }); +} + +main().catch((e) => { + if (e instanceof EnvError) { + console.error(e.message); + } else { + logger.error("起動エラー", e); + } + process.exit(1); +}); diff --git a/server/logger.ts b/server/logger.ts new file mode 100644 index 0000000..db3d286 --- /dev/null +++ b/server/logger.ts @@ -0,0 +1,10 @@ +const timestamp = () => new Date().toISOString(); + +export const logger = { + info: (msg: string, ...args: unknown[]) => + console.log(`[${timestamp()}] INFO ${msg}`, ...args), + warn: (msg: string, ...args: unknown[]) => + console.warn(`[${timestamp()}] WARN ${msg}`, ...args), + error: (msg: string, ...args: unknown[]) => + console.error(`[${timestamp()}] ERROR ${msg}`, ...args), +}; diff --git a/server/preflight.ts b/server/preflight.ts new file mode 100644 index 0000000..ac58553 --- /dev/null +++ b/server/preflight.ts @@ -0,0 +1,114 @@ +import { config as dotenvConfig } from "dotenv"; +import fs from "node:fs"; +import net from "node:net"; +import { REQUIRED_KEYS, DEFAULT_PORT } from "./constants.js"; + +const MIN_NODE_VERSION = 20; + +function fail(label: string, ...lines: string[]): never { + console.error(`\n[check] ${label} ... NG\n`); + for (const line of lines) console.error(line); + console.error(); + process.exit(1); +} + +function checkNodeVersion(): void { + const major = parseInt(process.versions.node.split(".")[0]!, 10); + if (major < MIN_NODE_VERSION) { + fail( + `Node.js v${process.versions.node}`, + ` Node.js v${MIN_NODE_VERSION} 以上が必要です(現在 v${process.versions.node})。`, + " https://nodejs.org/ から最新版をインストールしてください。", + ); + } +} + +function checkEnvFile(): void { + if (!fs.existsSync(".env")) { + fail( + ".env", + " .env ファイルが見つかりません。", + " 以下の手順で作成してください:", + "", + " 1. cp .env.example .env", + ' 2. テキストエディタで .env を開く(例: code .env)', + " 3. API キーを貼り付ける", + "", + " API キーの取得先:", + " Deepgram : https://console.deepgram.com/", + " Google AI : https://aistudio.google.com/apikey", + ); + } +} + +function checkRequiredKeys(): void { + for (const key of REQUIRED_KEYS) { + const val = process.env[key]; + if (!val || val.trim() === "") { + fail( + key, + ` ${key} が設定されていません。`, + " .env ファイルを開いて値を設定してください。", + "", + " API キーの取得先:", + " Deepgram : https://console.deepgram.com/", + " Google AI : https://aistudio.google.com/apikey", + ); + } + } +} + +function resolvePort(): number { + const portStr = process.env["PORT"] ?? String(DEFAULT_PORT); + const port = parseInt(portStr, 10); + if (isNaN(port) || port < 1 || port > 65535) { + fail("PORT", ` PORT の値が不正です: ${portStr}`); + } + return port; +} + +function checkPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.once("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + fail( + `ポート ${port}`, + ` ポート ${port} が使用中です。`, + " 前回のサーバーが残っている場合: just stop を実行", + " 他のアプリが使用中の場合: そのアプリを終了してください", + ); + } else if (err.code === "EACCES") { + fail( + `ポート ${port}`, + ` ポート ${port} にアクセス権がありません。`, + " 1024 以上のポート番号を .env の PORT に設定してください。", + ); + } else { + fail(`ポート ${port}`, ` ポートチェックでエラーが発生しました: ${err.message}`); + } + }); + server.listen({ port, host: "0.0.0.0" }, () => { + server.close(() => resolve()); + }); + }); +} + +async function main(): Promise { + checkNodeVersion(); + checkEnvFile(); + dotenvConfig(); + checkRequiredKeys(); + + const port = resolvePort(); + await checkPortAvailable(port); + + console.log( + `[check] 環境チェック完了 (Node.js v${process.versions.node}, .env, APIキー, ポート${port})`, + ); +} + +main().catch((e) => { + console.error("プリフライトチェックで予期しないエラーが発生しました:", e); + process.exit(1); +}); diff --git a/server/schemas.ts b/server/schemas.ts new file mode 100644 index 0000000..217f0de --- /dev/null +++ b/server/schemas.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const ExtractedTaskSchema = z.object({ + summary: z.string().describe("タスクの1行要約"), + assignee: z.string().nullable().describe("担当者名(推定)"), + deadline: z.string().nullable().describe("期限(言及があれば)"), + evidence: z.string().describe("根拠となる発話の引用"), +}); + +export const TaskExtractionResultSchema = z.object({ + tasks: z.array(ExtractedTaskSchema), +}); + +export type ExtractedTask = z.infer; +export type TaskExtractionResult = z.infer; diff --git a/server/task-extractor.ts b/server/task-extractor.ts new file mode 100644 index 0000000..c1f8b04 --- /dev/null +++ b/server/task-extractor.ts @@ -0,0 +1,190 @@ +import { setTimeout as delay } from "node:timers/promises"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { generateObject, APICallError } from "ai"; +import { google } from "@ai-sdk/google"; +import { getConfig } from "./config.js"; +import { TaskExtractionResultSchema, type ExtractedTask } from "./schemas.js"; +import type { TaskStatus } from "./types.js"; +import { logger } from "./logger.js"; + +export interface TaskExtractorOptions { + participants: string[]; + outputDir: string; + timestamp: string; + llmInputBuffer: string[]; + onStatusChange: (status: TaskStatus) => void; +} + +type TaskWithId = ExtractedTask & { id: string }; + +function isQuotaOrRateLimitError(error: unknown): boolean { + if (!APICallError.isInstance(error)) return false; + if (error.statusCode === 429) return true; + if (error.statusCode === 503) { + const body = error.responseBody ?? ""; + return body.includes("RESOURCE_EXHAUSTED") || body.includes("quota"); + } + return false; +} + +function buildPrompt(participants: string[], transcriptText: string): string { + const participantLine = participants.length > 0 + ? `参加者は ${participants.join("、")} です。` + : "参加者リストは未提供です。assignee は常に null にしてください。"; + + const assigneeRule = participants.length > 0 + ? `- assignee は参加者リスト(${participants.join("、")})の中からのみ選ぶ。リストにない名前を担当者にしてはならない +- 会話中で「〇〇さんお願い」「〇〇がやります」のように名前が明示されている場合のみ assignee を設定する +- 名前が明示されていない場合は assignee を null にする(推測しない)` + : "- assignee は常に null にする"; + + return `以下は会議の文字起こしの一部です。${participantLine} +話者の区別はありません(すべての発話が話者ラベルなしで記録されています)。 + +--- +${transcriptText} +--- + +上記の会話から以下を抽出してください。 + +タスク: +- 誰かが何かをやると約束した、または依頼された内容を抽出する +- 「検討します」「考えておきます」などの曖昧な表現もタスクとして抽出する +- 明確な行動(「作る」「送る」「確認する」「レビューする」等)だけでなく、検討・調査系の意思表示も含める +- evidence には、そのタスクの根拠となる発話を原文のまま引用する + +担当者の推定ルール: +${assigneeRule} +- 期限の言及がない場合は deadline を null にする + +エッジケース: +- テキストが2文以下の場合は tasks を空配列で返す +- タスクが見当たらない場合は tasks を空配列で返す(無理に抽出しない)`; +} + +function formatTasksMarkdown(tasks: TaskWithId[]): string { + const rows = tasks.map( + (t) => + `| ${t.id} | ${t.summary} | ${t.assignee ?? "-"} | ${t.deadline ?? "-"} | ${t.evidence} |`, + ); + + return [ + "# 抽出タスク", + "", + "| # | タスク | 担当 | 期限 | 根拠 |", + "|---|--------|------|------|------|", + ...rows, + "", + "---", + `*最終更新: ${new Date().toLocaleTimeString("ja-JP", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })}*`, + "", + ].join("\n"); +} + +async function abortableSleep(ms: number, signal: AbortSignal): Promise { + try { + await delay(ms, undefined, { signal }); + } catch { + // AbortError on cancellation — expected + } +} + +export async function taskExtractionLoop( + options: TaskExtractorOptions, + signal: AbortSignal, +): Promise { + const { participants, outputDir, timestamp, llmInputBuffer, onStatusChange } = options; + const cfg = getConfig().taskExtraction; + const tasksFilePath = path.join(outputDir, `meeting-${timestamp}-tasks.md`); + const allTasks: TaskWithId[] = []; + let taskCounter = 0; + let consecutiveFailures = 0; + + function computeBackoffMs(): number { + if (consecutiveFailures === 0) return cfg.intervalMs; + return Math.min( + cfg.intervalMs * Math.pow(2, consecutiveFailures - 1), + cfg.maxBackoffMs, + ); + } + + async function extractAndWrite(lines: string[]): Promise { + const transcriptText = lines.join("\n"); + logger.info(`タスク抽出開始 (${lines.length} 行, ${transcriptText.length} 文字)`); + + const { object } = await generateObject({ + model: google(cfg.model), + schema: TaskExtractionResultSchema, + prompt: buildPrompt(participants, transcriptText), + maxRetries: cfg.llmMaxRetries, + abortSignal: signal, + }); + + if (object.tasks.length > 0) { + const newTasks = object.tasks.map((t) => ({ + ...t, + id: String(++taskCounter), + })); + allTasks.push(...newTasks); + await fs.writeFile(tasksFilePath, formatTasksMarkdown(allTasks), "utf-8"); + logger.info(`タスク ${newTasks.length} 件抽出 → ${tasksFilePath}`); + } else { + logger.info("タスクなし(今回のサイクル)"); + } + } + + function currentStatus(overrides?: Partial): TaskStatus { + return { + taskCount: allTasks.length, + failing: consecutiveFailures > 0, + ...overrides, + }; + } + + while (!signal.aborted) { + if (llmInputBuffer.length > 0) { + const snapshot = llmInputBuffer.splice(0); + try { + await extractAndWrite(snapshot); + consecutiveFailures = 0; + onStatusChange(currentStatus()); + } catch (e) { + consecutiveFailures++; + const isQuota = isQuotaOrRateLimitError(e); + + if (isQuota) { + logger.warn( + `タスク抽出失敗: API クォータ超過 (連続 ${consecutiveFailures} 回)。Google AI Studio でプランを確認してください`, + ); + } else { + logger.error(`タスク抽出失敗 (連続 ${consecutiveFailures} 回)`, e); + } + + logger.warn(`失敗した ${snapshot.length} 行を破棄(次サイクルの新規バッファで再試行)`); + + const message = isQuota + ? "タスク抽出が API 制限で停止中" + : "タスク抽出でエラーが発生中"; + onStatusChange(currentStatus({ message })); + } + } + + await abortableSleep(computeBackoffMs(), signal); + } + + if (llmInputBuffer.length > 0 && consecutiveFailures < cfg.maxConsecutiveFailures) { + try { + logger.info("最終 flush: タスク抽出"); + await extractAndWrite(llmInputBuffer.splice(0)); + } catch { + logger.warn("最終 flush 失敗"); + } + } else if (llmInputBuffer.length > 0) { + logger.warn( + `最終 flush スキップ: API が ${consecutiveFailures} 回連続で失敗中のため`, + ); + } + + return currentStatus(); +} diff --git a/server/transcript-writer.ts b/server/transcript-writer.ts new file mode 100644 index 0000000..9b27a95 --- /dev/null +++ b/server/transcript-writer.ts @@ -0,0 +1,72 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { getConfig } from "./config.js"; + +export class TranscriptWriter { + private writeQueue = Promise.resolve(); + private readonly filePath: string; + + constructor(timestamp: string) { + this.filePath = path.join( + getConfig().output.dir, + `meeting-${timestamp}-transcript.md`, + ); + } + + async init(participants: string[]): Promise { + await fs.mkdir(path.dirname(this.filePath), { recursive: true }); + + const now = new Date(); + const dateStr = now.toLocaleDateString("ja-JP", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const timeStr = now.toLocaleTimeString("ja-JP", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const header = [ + `# 会議メモ ${dateStr} ${timeStr}`, + "", + "## 参加者", + participants.length > 0 ? participants.join("、") : "(未指定)", + "", + "---", + "", + ].join("\n"); + + await fs.writeFile(this.filePath, header, "utf-8"); + } + + appendUtterance( + text: string, + timestamp: string, + isSpeechFinal: boolean, + ): void { + if (!text.trim()) return; + + let content = `**[${timestamp}]** ${text}\n`; + if (isSpeechFinal) { + content += "\n"; + } + + this.append(content); + } + + private append(text: string): void { + this.writeQueue = this.writeQueue.then(() => + fs.appendFile(this.filePath, text, "utf-8"), + ); + } + + async flush(): Promise { + await this.writeQueue; + } + + getOutputPath(): string { + return this.filePath; + } +} diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..670a565 --- /dev/null +++ b/server/types.ts @@ -0,0 +1,17 @@ +export interface TaskStatus { + taskCount: number; + failing: boolean; + message?: string; +} + +export type ServerMessage = + | { type: "ready"; outputPath: string } + | { type: "transcript"; text: string; isFinal: boolean } + | { type: "reconnecting" } + | { type: "reconnected" } + | { type: "stopping" } + | { type: "stopped" } & TaskStatus + | { type: "task_status" } & TaskStatus + | { type: "error"; code: string; message: string }; + +export type ClientMessage = { type: "stop" }; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..4554c35 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,9 @@ +import { RecordingPanel } from "./components/recording-panel"; + +export function App() { + return ( +
+ +
+ ); +} diff --git a/src/components/recording-panel.tsx b/src/components/recording-panel.tsx new file mode 100644 index 0000000..bba5efb --- /dev/null +++ b/src/components/recording-panel.tsx @@ -0,0 +1,146 @@ +import { Mic, Square, RotateCcw, CheckCircle, AlertTriangle } from "lucide-react"; +import { useRecorder, type RecorderState, type CompletionNotice } from "../hooks/use-recorder"; +import { Button } from "./ui/button"; +import { Badge } from "./ui/badge"; + +export function RecordingPanel() { + const { state, start, stop, retry, completionNotice } = useRecorder(); + + return ( +
+
+

リアルタイム議事録

+ +
+ +
+ + +
+ + {state.status === "recording" && ( +

+ 出力先: {state.outputPath} +

+ )} + + {state.status === "error" && ( +

+ 録音済みの内容は保存されています +

+ )} + + {completionNotice && state.status === "idle" && ( + + )} +
+ ); +} + +const BADGE_CONFIG: Record = { + idle: { variant: "secondary", label: "待機中" }, + connecting: { variant: "warning", label: "接続中" }, + recording: { variant: "success", label: "録音中" }, + reconnecting: { variant: "warning", label: "再接続中", pulse: true }, + stopping: { variant: "secondary", label: "停止処理中" }, + error: { variant: "destructive", label: "エラー" }, +}; + +function StatusBadge({ status }: { status: RecorderState["status"] }) { + const cfg = BADGE_CONFIG[status]; + return ( + + {cfg.label} + + ); +} + +function ActionButton({ + state, + onStart, + onStop, + onRetry, +}: { + state: RecorderState; + onStart: () => void; + onStop: () => void; + onRetry: () => void; +}) { + switch (state.status) { + case "idle": + return ( + + ); + case "connecting": + case "stopping": + return ( + + ); + case "recording": + case "reconnecting": + return ( + + ); + case "error": + return ( + + ); + } +} + +function SnippetArea({ state }: { state: RecorderState }) { + switch (state.status) { + case "idle": + return null; + case "connecting": + return

接続しています...

; + case "recording": + return ( +
+

+ {state.lastTranscript ?? "音声を待っています..."} +

+ {state.taskFailing && ( +

+ {state.taskMessage ?? "タスク抽出でエラーが発生中"} +

+ )} +
+ ); + case "reconnecting": + return ( +

+ 接続を回復しています。音声は記録されています +

+ ); + case "stopping": + return

停止処理中...

; + case "error": + return

{state.message}

; + } +} + +function CompletionBanner({ notice }: { notice: CompletionNotice }) { + const Icon = notice.warning ? AlertTriangle : CheckCircle; + const colors = notice.warning + ? { border: "border-yellow-200", bg: "bg-yellow-50", icon: "text-yellow-600", text: "text-yellow-800" } + : { border: "border-green-200", bg: "bg-green-50", icon: "text-green-600", text: "text-green-800" }; + + return ( +
+ +

{notice.text}

+
+ ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..fce419e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,27 @@ +import type { HTMLAttributes } from "react"; +import { cn } from "../../lib/utils"; + +const badgeVariants = { + default: "bg-primary text-primary-foreground", + secondary: "bg-secondary text-secondary-foreground", + destructive: "bg-destructive/10 text-destructive border-destructive/20", + success: "bg-green-100 text-green-800 border-green-200", + warning: "bg-yellow-100 text-yellow-800 border-yellow-200", +}; + +export interface BadgeProps extends HTMLAttributes { + variant?: keyof typeof badgeVariants; +} + +export function Badge({ className, variant = "default", ...props }: BadgeProps) { + return ( +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..50ae0e0 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,30 @@ +import { forwardRef, type ButtonHTMLAttributes } from "react"; +import { cn } from "../../lib/utils"; + +const buttonVariants = { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", +}; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: keyof typeof buttonVariants; +} + +export const Button = forwardRef( + ({ className, variant = "default", ...props }, ref) => { + return ( +