mirror of
https://github.com/mssoftjp/obsidian-voice-input.git
synced 2026-07-22 16:40:28 +00:00
Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cde86540e | ||
|
|
fd15231bab | ||
|
|
d8a3200619 | ||
|
|
3249ed9811 | ||
|
|
5c04d21985 |
29 changed files with 2615 additions and 805 deletions
30
.github/workflows/ci.yml
vendored
Normal file
30
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepr:
|
||||
name: Pre-PR checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run checks
|
||||
run: npm run check:prepr
|
||||
57
README.md
57
README.md
|
|
@ -10,10 +10,10 @@ Capture notes with high-accuracy multilingual voice input using OpenAI Speech-to
|
|||
- Language separation: independent UI language and voice recognition language settings
|
||||
- Language linking: voice recognition follows the UI locale (ja/en/zh/ko) with optional overrides
|
||||
- AI post‑processing: optional dictionary-based cleanup (applied to all languages when enabled)
|
||||
- Quick controls in view: copy/clear/insert at cursor/append to end
|
||||
- Quick controls in view: copy/clear/insert at caret/append to end
|
||||
- Auto‑save drafts: periodic and on blur, automatic restore
|
||||
- Multilingual support: Japanese, English, Chinese, Korean interface languages
|
||||
- Voice activity detection modes: Off by default for maximum accuracy. Optional server-side chunking or local auto‑stop (requires fvad.wasm/fvad.js, installed manually)
|
||||
- Voice activity detection modes: Off by default for maximum accuracy. Optional server-side chunking or local auto‑stop (requires fvad.wasm, installed manually)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -31,7 +31,9 @@ Note: OpenAI usage is billed by API.
|
|||
|
||||
Compatibility: requires Obsidian 1.8.0 or later (per `minAppVersion`).
|
||||
|
||||
Local VAD (optional): this plugin does not ship the WebAssembly files. If you want local VAD auto‑stop, download `fvad.wasm` and `fvad.js` from the fvad‑wasm project and place them in the same plugin folder, or use the “Choose fvad.wasm / fvad.js…” button in Settings → Voice Activity Detection to copy them. The loader uses `fetch` only to read these local files—no external network requests are made. Local VAD is desktop‑only.
|
||||
Release assets: the plugin only needs `main.js`, `manifest.json`, and `styles.css` from the release bundle. Optional local VAD files are user-provided and are not included in releases.
|
||||
|
||||
Local VAD (optional): this plugin does not ship the WebAssembly file. If you want local VAD auto‑stop, download `fvad.wasm` from the fvad‑wasm project and place it in the same plugin folder, or use the “Choose fvad.wasm…” button in Settings → Voice Activity Detection to copy it. The plugin reads this local file from the installed plugin folder. No external network requests are made for local VAD. Local VAD is desktop‑only.
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -46,7 +48,7 @@ Local VAD (optional): this plugin does not ship the WebAssembly files. If you wa
|
|||
- Click “Start Voice Input” to toggle recording, or use push‑to‑talk: long‑press the record button (starts after a short threshold), release to stop.
|
||||
|
||||
3) Use the result
|
||||
- Copy, Insert at Cursor, or Append to End of the active note. Clear resets the area.
|
||||
- Copy, Insert at caret, or Append to end of the active note. Clear resets the area.
|
||||
|
||||
Tip: A settings gear in the view header opens the plugin’s settings.
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ Tip: A settings gear in the view header opens the plugin’s settings.
|
|||
- AI Post‑processing: enable dictionary‑based cleanup (applied to all languages when enabled)
|
||||
- Maximum Recording Duration: slider (default 5 min)
|
||||
- Plugin Language: Japanese/English/Chinese/Korean (controls UI display only, auto-detected from Obsidian, adjustable)
|
||||
- Voice Activity Detection: choose Off (default, most accurate), Server (faster turnaround via upstream silence trimming), or Local (desktop auto‑stop; requires `fvad.wasm`/`fvad.js` installed manually)
|
||||
- Voice Activity Detection: choose Off (default, most accurate), Server (faster turnaround via upstream silence trimming), or Local (desktop auto‑stop; requires `fvad.wasm` installed manually)
|
||||
|
||||
### Drafts and sync
|
||||
|
||||
|
|
@ -67,13 +69,16 @@ Tip: A settings gear in the view header opens the plugin’s settings.
|
|||
|
||||
## Security & Privacy
|
||||
|
||||
- Processing in memory; audio is not written to disk by the plugin
|
||||
- Audio you record is transmitted to OpenAI for transcription over HTTPS (via Obsidian’s `requestUrl`).
|
||||
- API key is encrypted for storage
|
||||
|
||||
Note: When Electron SafeStorage is unavailable, the plugin falls back to lightweight obfuscation for the stored key; the plugin is desktop‑only by design.
|
||||
|
||||
See also OpenAI’s Privacy Policy.
|
||||
- Processing in memory: audio is not written to disk by the plugin.
|
||||
- Network use: recorded audio is sent over HTTPS to `https://api.openai.com/v1/audio/transcriptions` via Obsidian’s `requestUrl` for transcription. The settings connection test calls `https://api.openai.com/v1/models`. No telemetry, ads, or self-update requests are sent by the plugin.
|
||||
- Account and billing: an OpenAI API key is required, and OpenAI API usage may be billed by OpenAI.
|
||||
- API key storage: the API key is stored in plugin settings. When Electron SafeStorage is available, the key is encrypted with SafeStorage before saving.
|
||||
- SafeStorage fallback: if Electron SafeStorage is unavailable, the plugin stores the key with a lightweight XOR/Base64 obfuscation fallback for backward compatibility. This fallback is not equivalent to OS-backed encryption.
|
||||
- Clipboard access: the Copy button writes only the current transcription text to the system clipboard. If note creation or insertion fails, the plugin may also write that same transcription text to the clipboard as a recovery fallback. The plugin does not read from the clipboard.
|
||||
- Vault file access: drafts are saved to, loaded from, and cleared from `<vault>/.obsidian/plugins/voice-input/draft.txt` using Obsidian Vault/FileManager APIs. Insert and append actions write only to the active target note, or create a timestamped `Voice-Memo-*.md` note when no suitable note is available.
|
||||
- Local files: optional local VAD reads `fvad.wasm` from the plugin folder after you install or choose it. The release bundle does not include this WebAssembly file.
|
||||
- External links: settings may show a link to the fvad-wasm GitHub project for manual download, but the plugin does not download those files automatically.
|
||||
- Privacy policy: see [OpenAI’s Privacy Policy](https://openai.com/policies/privacy-policy/) for OpenAI API data handling.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -100,7 +105,7 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
- ビュー内のクイック操作(コピー/クリア/カーソル位置へ挿入/末尾へ追記)
|
||||
- 自動保存(定期保存とフォーカス外れ時)。再オープン時に自動復元
|
||||
- 多言語サポート(日本語、英語、中国語、韓国語のインターフェース)
|
||||
- VADモード選択(標準はオフ=精度重視。必要に応じてサーバーVAD(応答を速めたい場合)や `fvad.wasm` / `fvad.js` を使ったローカルVAD(デスクトップの自動停止)を利用可能)
|
||||
- VADモード選択(標準はオフ=精度重視。必要に応じてサーバーVAD(応答を速めたい場合)や `fvad.wasm` を使ったローカルVAD(デスクトップの自動停止)を利用可能)
|
||||
|
||||
## 必要条件
|
||||
|
||||
|
|
@ -114,10 +119,11 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
|
||||
1. Releases から最新版を取得
|
||||
2. `main.js`、`manifest.json`、`styles.css` を `<Vault>/.obsidian/plugins/voice-input/` に配置
|
||||
3. `<Vault>/.obsidian/plugins/voice-input/` に置く
|
||||
4. Obsidianを再起動し、プラグインを有効化
|
||||
3. Obsidianを再起動し、プラグインを有効化
|
||||
|
||||
ローカルVAD(任意): 本プラグインは WebAssembly ファイルを同梱しません。ローカルVADの自動停止を使う場合、`fvad.wasm` と `fvad.js` を fvad‑wasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm / fvad.js を選択…」ボタンでコピーしてください。ローカルVADはデスクトップ専用です。
|
||||
リリースアセット: プラグインの実行に必要なファイルはリリースバンドル内の `main.js`、`manifest.json`、`styles.css` です。任意のローカルVADファイルはユーザーが用意するもので、リリースには含めません。
|
||||
|
||||
ローカルVAD(任意): 本プラグインは WebAssembly ファイルを同梱しません。ローカルVADの自動停止を使う場合、`fvad.wasm` を fvad‑wasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm を選択…」ボタンでコピーしてください。プラグインはインストール済みプラグインフォルダ内のこのローカルファイルを読み込みます。ローカルVADのための外部ネットワーク通信は行いません。ローカルVADはデスクトップ専用です。
|
||||
|
||||
## コマンド
|
||||
|
||||
|
|
@ -144,7 +150,7 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
- AI後処理: 辞書ベースの補正(有効時は全言語に適用)
|
||||
- 最大録音時間: スライダー(初期値5分)
|
||||
- **プラグイン言語**: UI表示のみを制御。Obsidianの言語設定から自動検出(ja/zh/ko/en)。
|
||||
- 音声区間検出 (VAD): オフ(標準・精度重視)、サーバー(応答を速めたい場合の無音カット)、ローカル(デスクトップの自動停止。`fvad.wasm`/`fvad.js` を手動導入)から選択
|
||||
- 音声区間検出 (VAD): オフ(標準・精度重視)、サーバー(応答を速めたい場合の無音カット)、ローカル(デスクトップの自動停止。`fvad.wasm` を手動導入)から選択
|
||||
|
||||
### ドラフトと同期
|
||||
|
||||
|
|
@ -173,13 +179,16 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
|
||||
## セキュリティ / プライバシー
|
||||
|
||||
- 処理はメモリ内で行い、音声ファイルはプラグイン側でディスク保存しません
|
||||
- 録音した音声は文字起こしのため OpenAI に送信され、HTTPS(Obsidian の `requestUrl` 経由)で通信します。
|
||||
- APIキーは保存時に暗号化
|
||||
|
||||
補足: Electron の SafeStorage が利用できない環境では保存キーを軽度に難読化して保持します(本プラグインはデスクトップ専用の設計です)。
|
||||
|
||||
OpenAIのプライバシーポリシーもご参照ください。
|
||||
- 処理はメモリ内で行い、音声ファイルはプラグイン側でディスク保存しません。
|
||||
- ネットワーク利用: 録音した音声は文字起こしのため `https://api.openai.com/v1/audio/transcriptions` に HTTPS(Obsidian の `requestUrl` 経由)で送信されます。設定画面の接続テストでは `https://api.openai.com/v1/models` に接続します。プラグインはテレメトリ、広告、自己更新のための通信を行いません。
|
||||
- アカウントと課金: OpenAI API キーが必要です。OpenAI API の利用料金は OpenAI 側で発生する場合があります。
|
||||
- APIキー保存: APIキーはプラグイン設定に保存されます。Electron SafeStorage が利用できる場合は、SafeStorage で暗号化して保存します。
|
||||
- SafeStorage フォールバック: Electron SafeStorage が利用できない場合、後方互換性のため XOR/Base64 による軽度の難読化で保存します。この方式は OS レベルの暗号化と同等ではありません。
|
||||
- クリップボードアクセス: コピーボタンは現在の文字起こし結果だけをシステムクリップボードへ書き込みます。ノート作成や挿入に失敗した場合も、復旧用 fallback として同じ文字起こし結果をクリップボードへ書き込むことがあります。プラグインはクリップボードからの読み取りは行いません。
|
||||
- Vault ファイルアクセス: 下書きは Obsidian の Vault/FileManager API を使って `<Vault>/.obsidian/plugins/voice-input/draft.txt` に保存、読み込み、削除されます。挿入と追記は対象ノートのみに書き込み、適切なノートが見つからない場合は `Voice-Memo-*.md` 形式のノートを作成します。
|
||||
- ローカルファイル: 任意のローカルVADは、ユーザーが配置または選択した `fvad.wasm` をプラグインフォルダから読み込みます。リリースバンドルにはこの WebAssembly ファイルは含めません。
|
||||
- 外部リンク: 設定画面に fvad-wasm の GitHub プロジェクトへのリンクを表示することがありますが、プラグインが自動でこれらのファイルをダウンロードすることはありません。
|
||||
- プライバシーポリシー: OpenAI API におけるデータの扱いは [OpenAI のプライバシーポリシー](https://openai.com/policies/privacy-policy/) も参照してください。
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import { builtinModules } from "node:module";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const builtins = [...new Set(builtinModules.flatMap((name) => (
|
||||
name.startsWith("node:") ? [name] : [name, `node:${name}`]
|
||||
)))];
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ const manifestJsonParser = {
|
|||
}
|
||||
};
|
||||
|
||||
const obsidianmdManifestRuleOverrides = Object.fromEntries(
|
||||
Object.keys(obsidianmd.rules).map((ruleName) => [`obsidianmd/${ruleName}`, 'off'])
|
||||
);
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
ignores: [
|
||||
|
|
@ -49,6 +53,7 @@ export default defineConfig([
|
|||
obsidianmd
|
||||
},
|
||||
rules: {
|
||||
...obsidianmdManifestRuleOverrides,
|
||||
'obsidianmd/validate-manifest': 'error'
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "voice-input",
|
||||
"name": "Voice Input",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.5",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Capture notes with high-accuracy multilingual voice input using OpenAI Speech-to-Text.",
|
||||
"author": "Musashino Software",
|
||||
|
|
|
|||
2917
package-lock.json
generated
2917
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
|
@ -1,8 +1,11 @@
|
|||
{
|
||||
"name": "voice-input",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.5",
|
||||
"description": "Voice input plugin for Obsidian with accurate transcription",
|
||||
"main": "main.js",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
|
|
@ -14,8 +17,8 @@
|
|||
"check": "npm run lint && npm run build",
|
||||
"check:artifacts": "node scripts/check-artifacts.mjs",
|
||||
"check:prepr": "npm run lint && npm test && npm run build-plugin && npm run check:artifacts",
|
||||
"lint": "eslint src manifest.json --ext .ts",
|
||||
"lint:fix": "eslint src manifest.json --ext .ts --fix",
|
||||
"lint": "eslint src manifest.json --ext .ts --max-warnings=0",
|
||||
"lint:fix": "eslint src manifest.json --ext .ts --fix --max-warnings=0",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
|
|
@ -33,25 +36,23 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/json": "0.14.0",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-obsidianmd": "0.1.9",
|
||||
"eslint-plugin-obsidianmd": "0.3.0",
|
||||
"globals": "16.5.0",
|
||||
"jest": "30.0.5",
|
||||
"jest-environment-jsdom": "^30.0.5",
|
||||
"obsidian": "1.8.7",
|
||||
"ts-jest": "29.4.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@echogarden/fvad-wasm": "^0.2.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"jest": "^30.0.5",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"ts-jest": "^29.4.0"
|
||||
"@echogarden/fvad-wasm": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@ export const FILE_CONSTANTS = {
|
|||
|
||||
/** WASMファイルのパス */
|
||||
WASM_PATHS: {
|
||||
JS: 'src/lib/fvad-wasm/fvad.js',
|
||||
WASM: 'src/lib/fvad-wasm/fvad.wasm'
|
||||
}
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class AudioRecorder extends Disposable {
|
|||
private isRecording: boolean = false;
|
||||
private stream: MediaStream | null = null;
|
||||
private options: AudioRecorderOptions;
|
||||
private silenceTimer: NodeJS.Timeout | null = null;
|
||||
private silenceTimer: number | null = null;
|
||||
private lastSpeechTime: number = 0;
|
||||
private continuousAudioData: Float32Array[] = [];
|
||||
private audioRingBuffer: AudioRingBuffer;
|
||||
|
|
@ -50,7 +50,7 @@ export class AudioRecorder extends Disposable {
|
|||
private disposables: CompositeDisposable = new CompositeDisposable();
|
||||
private logger: Logger | null = null;
|
||||
private recordingStartTime: number = 0;
|
||||
private maxRecordingTimer: NodeJS.Timeout | null = null;
|
||||
private maxRecordingTimer: number | null = null;
|
||||
private isStarting: boolean = false;
|
||||
|
||||
constructor(options: AudioRecorderOptions) {
|
||||
|
|
@ -279,7 +279,7 @@ export class AudioRecorder extends Disposable {
|
|||
|
||||
// Set up maximum recording time limit
|
||||
const maxSeconds = this.options.maxRecordingSeconds || AUDIO_CONSTANTS.MAX_RECORDING_SECONDS;
|
||||
this.maxRecordingTimer = setTimeout(() => {
|
||||
this.maxRecordingTimer = window.setTimeout(() => {
|
||||
this.logger?.info(`Maximum recording time (${maxSeconds}s) reached, stopping recording`);
|
||||
void this.stopRecording()
|
||||
.then((audioBlob) => {
|
||||
|
|
@ -401,10 +401,10 @@ export class AudioRecorder extends Disposable {
|
|||
this.logger?.error('Failed to process audio data from analyser fallback', error);
|
||||
}
|
||||
|
||||
this.analyserFrameRequest = requestAnimationFrame(processFrame);
|
||||
this.analyserFrameRequest = window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
this.analyserFrameRequest = requestAnimationFrame(processFrame);
|
||||
this.analyserFrameRequest = window.requestAnimationFrame(processFrame);
|
||||
}
|
||||
|
||||
private stopAnalyserProcessing(): void {
|
||||
|
|
@ -489,7 +489,7 @@ export class AudioRecorder extends Disposable {
|
|||
this.lastSpeechTime = Date.now();
|
||||
|
||||
if (this.silenceTimer) {
|
||||
clearTimeout(this.silenceTimer);
|
||||
window.clearTimeout(this.silenceTimer);
|
||||
this.silenceTimer = null;
|
||||
}
|
||||
|
||||
|
|
@ -514,7 +514,7 @@ export class AudioRecorder extends Disposable {
|
|||
// Check if we should auto-stop due to silence (only in VAD mode)
|
||||
if (this.options.useVAD && this.lastSpeechTime > 0 && !this.silenceTimer) {
|
||||
const autoStopDuration = this.options.autoStopSilenceDuration ?? DEFAULT_AUDIO_SETTINGS.autoStopSilenceDuration;
|
||||
this.silenceTimer = setTimeout(() => {
|
||||
this.silenceTimer = window.setTimeout(() => {
|
||||
if (!this.isRecording) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -543,7 +543,7 @@ export class AudioRecorder extends Disposable {
|
|||
|
||||
// Clear maximum recording timer
|
||||
if (this.maxRecordingTimer) {
|
||||
clearTimeout(this.maxRecordingTimer);
|
||||
window.clearTimeout(this.maxRecordingTimer);
|
||||
this.maxRecordingTimer = null;
|
||||
}
|
||||
|
||||
|
|
@ -576,12 +576,12 @@ export class AudioRecorder extends Disposable {
|
|||
|
||||
private cleanup(): void {
|
||||
if (this.silenceTimer) {
|
||||
clearTimeout(this.silenceTimer);
|
||||
window.clearTimeout(this.silenceTimer);
|
||||
this.silenceTimer = null;
|
||||
}
|
||||
|
||||
if (this.maxRecordingTimer) {
|
||||
clearTimeout(this.maxRecordingTimer);
|
||||
window.clearTimeout(this.maxRecordingTimer);
|
||||
this.maxRecordingTimer = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class AudioVisualizer extends Disposable {
|
|||
|
||||
constructor(container: HTMLElement) {
|
||||
super();
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas = activeDocument.createElement('canvas');
|
||||
this.canvas.className = 'voice-input-audio-visualizer-canvas';
|
||||
this.canvas.width = 200;
|
||||
this.canvas.height = 56;
|
||||
|
|
@ -46,7 +46,7 @@ export class AudioVisualizer extends Disposable {
|
|||
stop(): void {
|
||||
this.isRecording = false;
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
window.cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
this.clear();
|
||||
|
|
@ -65,7 +65,7 @@ export class AudioVisualizer extends Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.draw());
|
||||
this.animationId = window.requestAnimationFrame(() => this.draw());
|
||||
|
||||
// Get time domain data (waveform)
|
||||
const waveformArray = this.dataArray as Uint8Array<ArrayBuffer>;
|
||||
|
|
@ -230,20 +230,20 @@ export class SimpleAudioLevelIndicator extends Disposable {
|
|||
}
|
||||
|
||||
private createUI(): void {
|
||||
const wrapper = document.createElement('div');
|
||||
const wrapper = activeDocument.createElement('div');
|
||||
wrapper.className = 'voice-input-audio-level-indicator';
|
||||
|
||||
// Level bar
|
||||
const levelContainer = document.createElement('div');
|
||||
const levelContainer = activeDocument.createElement('div');
|
||||
levelContainer.className = 'voice-input-audio-level-container';
|
||||
|
||||
this.levelBar = document.createElement('div');
|
||||
this.levelBar = activeDocument.createElement('div');
|
||||
this.levelBar.className = 'voice-input-audio-level-bar';
|
||||
|
||||
levelContainer.appendChild(this.levelBar);
|
||||
|
||||
// VAD indicator
|
||||
this.vadIndicator = document.createElement('div');
|
||||
this.vadIndicator = activeDocument.createElement('div');
|
||||
this.vadIndicator.className = 'voice-input-audio-vad-indicator';
|
||||
|
||||
wrapper.appendChild(levelContainer);
|
||||
|
|
@ -265,7 +265,7 @@ export class SimpleAudioLevelIndicator extends Disposable {
|
|||
stop(): void {
|
||||
this.isActive = false;
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
window.cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
this.setLevelBarClass(0);
|
||||
|
|
@ -284,7 +284,7 @@ export class SimpleAudioLevelIndicator extends Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.update());
|
||||
this.animationId = window.requestAnimationFrame(() => this.update());
|
||||
|
||||
const frequencyArray = this.dataArray as Uint8Array<ArrayBuffer>;
|
||||
this.analyser.getByteFrequencyData(frequencyArray);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { App, FileSystemAdapter, normalizePath } from 'obsidian';
|
||||
import createFvadModule from '@echogarden/fvad-wasm';
|
||||
import { VAD_CONSTANTS, DEFAULT_VAD_SETTINGS, FILE_CONSTANTS } from '../../config';
|
||||
import { Disposable } from '../../interfaces';
|
||||
import { FvadModule, WindowWithFvad, hasFvadModule } from '../../types';
|
||||
import { FvadModule } from '../../types';
|
||||
import { createServiceLogger } from '../../services';
|
||||
import { Logger } from '../../utils';
|
||||
|
||||
|
|
@ -104,85 +105,12 @@ export class VADProcessor extends Disposable {
|
|||
* fvad モジュールを初期化
|
||||
*/
|
||||
private async initializeFvadModule(wasmBuffer: ArrayBuffer): Promise<void> {
|
||||
// スクリプトタグを使用して fvad.js を読み込む
|
||||
// これは Obsidian の制限された環境でモジュールを読み込む最も確実な方法
|
||||
this.ensureFileSystemAdapter();
|
||||
|
||||
// グローバルオブジェクトを準備(型安全に)
|
||||
const globalWindow = window as WindowWithFvad;
|
||||
|
||||
// fvad.js の内容を読み込んで評価(プラグインルートから)
|
||||
const fvadJsPath = this.getPluginAssetPath('fvad.js');
|
||||
const fvadJsContent = await this.readPluginTextAsset('fvad.js');
|
||||
const sanitizedFvadJsContent = fvadJsContent
|
||||
// Remove block comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
// Remove line comments
|
||||
.replace(/^\s*\/\/.*$/gm, '');
|
||||
|
||||
// モジュールを評価するための一時的な環境を作成
|
||||
return new Promise((resolve, reject) => {
|
||||
// スクリプトタグを作成
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.textContent =
|
||||
`const importMeta={url:'file:///${fvadJsPath}'};` +
|
||||
`${sanitizedFvadJsContent}` +
|
||||
`window.__fvadModule=fvad;`;
|
||||
|
||||
// エラーハンドリング
|
||||
script.onerror = (error) => {
|
||||
this.logger.error('WebRTC VAD script loading error', error);
|
||||
reject(new Error('Failed to load fvad.js'));
|
||||
};
|
||||
|
||||
// スクリプトを実行
|
||||
document.head.appendChild(script);
|
||||
|
||||
// モジュールが読み込まれるのを待つ
|
||||
setTimeout(() => {
|
||||
const loadModule = async (): Promise<void> => {
|
||||
if (!hasFvadModule(globalWindow)) {
|
||||
throw new Error('fvad module not found in global scope');
|
||||
}
|
||||
|
||||
const createModule = globalWindow.__fvadModule;
|
||||
if (!createModule) {
|
||||
throw new Error('fvad module factory is undefined');
|
||||
}
|
||||
this.logger.debug('fvad module loaded from global scope');
|
||||
|
||||
// WebAssembly モジュールを初期化
|
||||
this.fvadModule = await createModule({
|
||||
wasmBinary: new Uint8Array(wasmBuffer),
|
||||
instantiateWasm: (
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (instance: WebAssembly.Instance) => void
|
||||
) => {
|
||||
WebAssembly.instantiate(new Uint8Array(wasmBuffer), imports)
|
||||
.then((result) => {
|
||||
successCallback(result.instance);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error('WebRTC VAD WASM instantiation error', error);
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
// クリーンアップ
|
||||
document.head.removeChild(script);
|
||||
delete globalWindow.__fvadModule;
|
||||
};
|
||||
|
||||
loadModule()
|
||||
.then(() => resolve())
|
||||
.catch((error) => {
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
}, 100); // モジュール読み込みを待つ
|
||||
this.fvadModule = await createFvadModule({
|
||||
wasmBinary: new Uint8Array(wasmBuffer)
|
||||
});
|
||||
this.logger.debug('fvad module initialized from bundled loader');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -243,21 +171,6 @@ export class VADProcessor extends Disposable {
|
|||
return normalizePath(`${this.getPluginAssetBasePath()}/${fileName}`);
|
||||
}
|
||||
|
||||
private async readPluginTextAsset(fileName: string): Promise<string> {
|
||||
const fullPath = this.getPluginAssetPath(fileName);
|
||||
const existingFile = this.app.vault.getFileByPath(fullPath);
|
||||
if (existingFile) {
|
||||
return this.app.vault.read(existingFile);
|
||||
}
|
||||
|
||||
const adapter = this.ensureFileSystemAdapter();
|
||||
const exists = await adapter.exists(fullPath);
|
||||
if (!exists) {
|
||||
throw new Error(`Asset not found: ${fullPath}`);
|
||||
}
|
||||
return adapter.read(fullPath);
|
||||
}
|
||||
|
||||
private async readPluginBinaryAsset(fileName: string): Promise<ArrayBuffer> {
|
||||
const fullPath = this.getPluginAssetPath(fileName);
|
||||
const existingFile = this.app.vault.getFileByPath(fullPath);
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ export class ErrorHandler implements IDisposable {
|
|||
* 遅延処理
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise(resolve => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const en: TranslationResource = {
|
|||
serviceInitFailed: 'Service initialization failed',
|
||||
audioTooShort: 'Audio is too short',
|
||||
noAudioDetected: 'No audio detected',
|
||||
localVadMissing: 'Local VAD module not found. Using server-side detection instead. Place fvad.wasm and fvad.js under {path}.'
|
||||
localVadMissing: 'Local VAD module not found. Using server-side detection instead. Place fvad.wasm under {path}.'
|
||||
},
|
||||
error: {
|
||||
clipboardFailed: 'Failed to copy to clipboard',
|
||||
|
|
@ -131,7 +131,7 @@ export const en: TranslationResource = {
|
|||
cleanup: 'Clean up',
|
||||
copy: 'Copy',
|
||||
insert: 'Insert to note',
|
||||
insertAtCursor: 'Insert at cursor',
|
||||
insertAtCursor: 'Insert at caret',
|
||||
append: 'Append to end',
|
||||
clear: 'Clear',
|
||||
cancel: 'Cancel',
|
||||
|
|
@ -180,20 +180,20 @@ export const en: TranslationResource = {
|
|||
dictionaryImportExport: 'Dictionary import/export',
|
||||
dictionaryImportExportDesc: 'Import or export your correction dictionary as JSON',
|
||||
vadMode: 'Voice activity detection',
|
||||
vadModeDesc: 'Off (default) keeps the raw audio for maximum accuracy. Server VAD can trim silence upstream for quicker turnaround but may slightly affect segmentation accuracy. Local VAD (requires fvad.wasm/fvad.js) stops recording automatically on the desktop.',
|
||||
vadModeLocalMissing: 'Local VAD requires fvad.wasm and fvad.js under {path}. Install them before switching.',
|
||||
vadModeDesc: 'Off keeps the raw audio for maximum accuracy. Server VAD can trim silence upstream for quicker turnaround but may slightly affect segmentation accuracy. Local VAD requires fvad.wasm and stops recording automatically on the desktop.',
|
||||
vadModeLocalMissing: 'Local VAD requires fvad.wasm under {path}. Install it before switching.',
|
||||
vadModeLocalAvailable: 'Local VAD files found in {path}. Recording will auto-stop on silence.',
|
||||
vadModeDisabledDesc: 'Voice activity detection is disabled – audio is recorded continuously and sent untouched.',
|
||||
vadModeSummaryServer: 'Cuts silence on the server for faster turnaround (cloud processing).',
|
||||
vadModeSummaryLocal: 'Detects silence locally and auto-stops on desktop (requires fvad.wasm/fvad.js).',
|
||||
vadModeInstallButton: 'Choose fvad.wasm / fvad.js…',
|
||||
vadModeInstallDesc: 'Download fvad.wasm (and fvad.js) from the official WebRTC voice activity detection port, then choose the wasm file to copy it into the plugin folder (desktop only).',
|
||||
vadModeSummaryLocal: 'Detects silence locally and auto-stops on desktop (requires fvad.wasm).',
|
||||
vadModeInstallButton: 'Choose fvad.wasm…',
|
||||
vadModeInstallDesc: 'Download fvad.wasm from the official WebRTC voice activity detection port, then choose the wasm file to copy it into the plugin folder (desktop only).',
|
||||
vadModeInstallLinkLabel: 'Visit the fvad-wasm project',
|
||||
vadModeInstallInvalidName: 'Please select a file named fvad.wasm.',
|
||||
vadModeInstallInvalidType: 'The selected file does not appear to be a WebAssembly module.',
|
||||
vadModeInstallSuccess: 'The fvad.wasm file is installed. Local voice activity detection will run the next time you record.',
|
||||
vadModeInstallWriteError: 'Failed to install VAD assets: {error}',
|
||||
vadModeInstallJsMissing: 'Local voice activity detection also needs fvad.js in the same plugin folder. Copy it manually if it is not present.'
|
||||
vadModeInstallJsMissing: 'Local voice activity detection now only requires fvad.wasm. Update the plugin if this message appears.'
|
||||
},
|
||||
options: {
|
||||
modelMini: 'GPT-4o mini transcribe',
|
||||
|
|
@ -209,8 +209,8 @@ export const en: TranslationResource = {
|
|||
},
|
||||
tooltips: {
|
||||
copy: 'Copy to clipboard',
|
||||
insert: 'Insert at cursor',
|
||||
insertAtCursor: 'Insert at cursor position',
|
||||
insert: 'Insert at caret',
|
||||
insertAtCursor: 'Insert at caret position',
|
||||
append: 'Append to end of note',
|
||||
clear: 'Press twice to clear text area',
|
||||
settingsButton: 'Open settings'
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const ja: TranslationResource = {
|
|||
serviceInitFailed: 'サービスの初期化に失敗しました',
|
||||
audioTooShort: '音声が短すぎます',
|
||||
noAudioDetected: '音声が検出されませんでした',
|
||||
localVadMissing: 'ローカルVADモジュールが見つからないため、サーバーVADに切り替えます。{path} に fvad.wasm と fvad.js を配置してください。'
|
||||
localVadMissing: 'ローカルVADモジュールが見つからないため、サーバーVADに切り替えます。{path} に fvad.wasm を配置してください。'
|
||||
},
|
||||
error: {
|
||||
clipboardFailed: 'クリップボードへのコピーに失敗しました',
|
||||
|
|
@ -180,20 +180,20 @@ export const ja: TranslationResource = {
|
|||
dictionaryImportExport: '辞書のインポート/エクスポート',
|
||||
dictionaryImportExportDesc: '補正辞書をJSONファイルとしてインポート・エクスポート',
|
||||
vadMode: '音声区間検出(VAD)',
|
||||
vadModeDesc: 'オフ(標準)は録音をそのまま送信するため精度重視です。サーバーVADを有効にすると無音区間を先にカットし、応答が速くなる場合がありますが、切り分けによって文字起こし精度に影響する可能性があります。ローカルVAD(fvad.wasm/fvad.js が必要)はPC側で無音を検出し自動停止します。',
|
||||
vadModeLocalMissing: 'ローカルVADを有効にするには {path} に fvad.wasm と fvad.js を配置してください。',
|
||||
vadModeDesc: 'オフ(標準)は録音をそのまま送信するため精度重視です。サーバーVADを有効にすると無音区間を先にカットし、応答が速くなる場合がありますが、切り分けによって文字起こし精度に影響する可能性があります。ローカルVAD(fvad.wasm が必要)はPC側で無音を検出し自動停止します。',
|
||||
vadModeLocalMissing: 'ローカルVADを有効にするには {path} に fvad.wasm を配置してください。',
|
||||
vadModeLocalAvailable: '{path} でローカルVAD用モジュールを検出しました。無音で自動停止します。',
|
||||
vadModeDisabledDesc: 'VADを無効化すると録音は常に継続し、加工せず送信されます。',
|
||||
vadModeSummaryServer: 'サーバー側で無音をカット(応答を速めたい場合)',
|
||||
vadModeSummaryLocal: 'デスクトップで無音を検出して自動停止(fvad.wasm/fvad.js 必須)',
|
||||
vadModeInstallButton: 'fvad.wasm / fvad.js を選択…',
|
||||
vadModeInstallDesc: '公式の WebRTC VAD 移植プロジェクトから fvad.wasm(および fvad.js)をダウンロードし、ここで wasm ファイルを選択するとプラグインフォルダにコピーされます(デスクトップのみ)。',
|
||||
vadModeSummaryLocal: 'デスクトップで無音を検出して自動停止(fvad.wasm 必須)',
|
||||
vadModeInstallButton: 'fvad.wasm を選択…',
|
||||
vadModeInstallDesc: '公式の WebRTC VAD 移植プロジェクトから fvad.wasm をダウンロードし、ここで wasm ファイルを選択するとプラグインフォルダにコピーされます(デスクトップのみ)。',
|
||||
vadModeInstallLinkLabel: 'fvad-wasm プロジェクト',
|
||||
vadModeInstallInvalidName: 'fvad.wasm というファイルを選択してください。',
|
||||
vadModeInstallInvalidType: 'WebAssembly モジュールではないファイルが選択されました。',
|
||||
vadModeInstallSuccess: 'fvad.wasm を配置しました。次回の録音からローカルVADを試行します。',
|
||||
vadModeInstallWriteError: 'VAD関連ファイルの配置に失敗しました: {error}',
|
||||
vadModeInstallJsMissing: 'ローカルVADには fvad.js も同じフォルダに必要です。存在しない場合は手動で配置してください。'
|
||||
vadModeInstallJsMissing: '現在のローカルVADは fvad.wasm のみを必要とします。この表示が出る場合はプラグインを更新してください。'
|
||||
},
|
||||
options: {
|
||||
modelMini: 'GPT-4o mini Transcribe',
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const ko: TranslationResource = {
|
|||
serviceInitFailed: '서비스 초기화에 실패했습니다',
|
||||
audioTooShort: '오디오가 너무 짧습니다',
|
||||
noAudioDetected: '오디오가 감지되지 않았습니다',
|
||||
localVadMissing: '로컬 VAD 모듈을 찾을 수 없어 서버 VAD로 전환합니다. {path} 에 fvad.wasm 및 fvad.js 를 배치하세요.'
|
||||
localVadMissing: '로컬 VAD 모듈을 찾을 수 없어 서버 VAD로 전환합니다. {path} 에 fvad.wasm 을 배치하세요.'
|
||||
},
|
||||
error: {
|
||||
clipboardFailed: '클립보드 복사에 실패했습니다',
|
||||
|
|
@ -180,20 +180,20 @@ export const ko: TranslationResource = {
|
|||
dictionaryImportExport: '사전 가져오기/내보내기',
|
||||
dictionaryImportExportDesc: '교정 사전을 JSON 파일로 가져오기 또는 내보내기',
|
||||
vadMode: '음성 활동 감지(VAD)',
|
||||
vadModeDesc: '기본값인 “끄기”는 원본 오디오를 그대로 전송하여 최고 정확도를 유지합니다. 서버 VAD를 켜면 업로드 전에 무음을 잘라 전체 응답 속도가 빨라질 수 있지만, 분할 방식에 따라 정확도에 영향이 있을 수 있습니다. 로컬 VAD(fvad.wasm / fvad.js 필요)는 데스크톱에서 무음을 감지해 자동으로 녹음을 멈춥니다.',
|
||||
vadModeLocalMissing: '{path} 에 fvad.wasm 및 fvad.js 를 배치하면 로컬 VAD를 사용할 수 있습니다.',
|
||||
vadModeDesc: '기본값인 “끄기”는 원본 오디오를 그대로 전송하여 최고 정확도를 유지합니다. 서버 VAD를 켜면 업로드 전에 무음을 잘라 전체 응답 속도가 빨라질 수 있지만, 분할 방식에 따라 정확도에 영향이 있을 수 있습니다. 로컬 VAD(fvad.wasm 필요)는 데스크톱에서 무음을 감지해 자동으로 녹음을 멈춥니다.',
|
||||
vadModeLocalMissing: '{path} 에 fvad.wasm 을 배치하면 로컬 VAD를 사용할 수 있습니다.',
|
||||
vadModeLocalAvailable: '{path} 에서 로컬 VAD 모듈을 감지했습니다. 무음 시 자동으로 녹음을 중지합니다.',
|
||||
vadModeDisabledDesc: 'VAD 끄기: 무음이 있어도 녹음을 계속 진행합니다.',
|
||||
vadModeSummaryServer: '서버에서 무음을 잘라 응답을 빠르게 함 (클라우드)',
|
||||
vadModeSummaryLocal: '데스크톱에서 무음을 감지해 자동 중지 (fvad.wasm/fvad.js 필요)',
|
||||
vadModeInstallButton: 'fvad.wasm / fvad.js 선택…',
|
||||
vadModeInstallDesc: '공식 WebRTC VAD 포트에서 fvad.wasm(및 fvad.js)을 다운로드한 뒤 여기서 wasm 파일을 선택하면 플러그인 폴더에 복사됩니다(데스크톱 한정).',
|
||||
vadModeSummaryLocal: '데스크톱에서 무음을 감지해 자동 중지 (fvad.wasm 필요)',
|
||||
vadModeInstallButton: 'fvad.wasm 선택…',
|
||||
vadModeInstallDesc: '공식 WebRTC VAD 포트에서 fvad.wasm을 다운로드한 뒤 여기서 wasm 파일을 선택하면 플러그인 폴더에 복사됩니다(데스크톱 한정).',
|
||||
vadModeInstallLinkLabel: 'fvad-wasm 프로젝트',
|
||||
vadModeInstallInvalidName: '파일 이름이 fvad.wasm 인 것을 선택해주세요.',
|
||||
vadModeInstallInvalidType: '선택한 파일이 WebAssembly 모듈이 아닌 것 같습니다.',
|
||||
vadModeInstallSuccess: 'fvad.wasm 설치 완료. 다음 녹음부터 로컬 VAD를 시도합니다.',
|
||||
vadModeInstallWriteError: 'VAD 관련 파일 설치에 실패했습니다: {error}',
|
||||
vadModeInstallJsMissing: '로컬 VAD에는 fvad.js 도 필요합니다. 없을 경우 동일한 폴더에 직접 복사해 주세요.'
|
||||
vadModeInstallJsMissing: '현재 로컬 VAD는 fvad.wasm만 필요합니다. 이 메시지가 표시되면 플러그인을 업데이트하세요.'
|
||||
},
|
||||
options: {
|
||||
modelMini: 'GPT-4o mini Transcribe',
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const zh: TranslationResource = {
|
|||
serviceInitFailed: '服务初始化失败',
|
||||
audioTooShort: '音频过短',
|
||||
noAudioDetected: '未检测到音频',
|
||||
localVadMissing: '未找到本地VAD模块,已切换为服务器端检测。请将 fvad.wasm 和 fvad.js 放在 {path}。'
|
||||
localVadMissing: '未找到本地VAD模块,已切换为服务器端检测。请将 fvad.wasm 放在 {path}。'
|
||||
},
|
||||
error: {
|
||||
clipboardFailed: '复制到剪贴板失败',
|
||||
|
|
@ -180,20 +180,20 @@ export const zh: TranslationResource = {
|
|||
dictionaryImportExport: '词典导入/导出',
|
||||
dictionaryImportExportDesc: '将校正词典作为JSON文件导入或导出',
|
||||
vadMode: '语音活动检测(VAD)',
|
||||
vadModeDesc: '默认的“关闭”会保持原始音频,以获得最高的转写精度。启用服务器VAD可以在上传前剪掉静音,可能缩短整体响应时间,但也可能影响分段精度。本地VAD(需要 fvad.wasm / fvad.js)会在桌面端检测静音并自动停止录音。',
|
||||
vadModeLocalMissing: '启用本地VAD需要将 fvad.wasm 和 fvad.js 放在 {path}。',
|
||||
vadModeDesc: '默认的“关闭”会保持原始音频,以获得最高的转写精度。启用服务器VAD可以在上传前剪掉静音,可能缩短整体响应时间,但也可能影响分段精度。本地VAD(需要 fvad.wasm)会在桌面端检测静音并自动停止录音。',
|
||||
vadModeLocalMissing: '启用本地VAD需要将 fvad.wasm 放在 {path}。',
|
||||
vadModeLocalAvailable: '在 {path} 检测到本地VAD模块,将在静音时自动停止录音。',
|
||||
vadModeDisabledDesc: '关闭VAD:持续录音并完整发送音频。',
|
||||
vadModeSummaryServer: '服务器裁剪静音,加快整体响应(云端处理)',
|
||||
vadModeSummaryLocal: '在桌面本地检测静音并自动停止(需 fvad.wasm/fvad.js)',
|
||||
vadModeInstallButton: '选择 fvad.wasm / fvad.js…',
|
||||
vadModeInstallDesc: '请从官方 WebRTC VAD 移植项目下载 fvad.wasm(以及 fvad.js),在此选择 wasm 文件即可复制到插件目录(仅桌面)。',
|
||||
vadModeSummaryLocal: '在桌面本地检测静音并自动停止(需 fvad.wasm)',
|
||||
vadModeInstallButton: '选择 fvad.wasm…',
|
||||
vadModeInstallDesc: '请从官方 WebRTC VAD 移植项目下载 fvad.wasm,在此选择 wasm 文件即可复制到插件目录(仅桌面)。',
|
||||
vadModeInstallLinkLabel: 'fvad-wasm 项目',
|
||||
vadModeInstallInvalidName: '请选择名为 fvad.wasm 的文件。',
|
||||
vadModeInstallInvalidType: '所选文件不像 WebAssembly 模块。',
|
||||
vadModeInstallSuccess: '已安装 fvad.wasm。下次录音将尝试本地 VAD。',
|
||||
vadModeInstallWriteError: '安装 VAD 相关文件失败:{error}',
|
||||
vadModeInstallJsMissing: '本地 VAD 还需要 fvad.js。若不存在,请手动复制到同一插件目录。'
|
||||
vadModeInstallJsMissing: '当前本地 VAD 只需要 fvad.wasm。如果看到此消息,请更新插件。'
|
||||
},
|
||||
options: {
|
||||
modelMini: 'GPT-4o mini Transcribe',
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export default class VoiceInputPlugin extends Plugin {
|
|||
// Add timeout to prevent hanging
|
||||
return Promise.race([
|
||||
view.actions.stopRecording(),
|
||||
new Promise(resolve => setTimeout(resolve, 1000)) // 1 second timeout
|
||||
new Promise(resolve => window.setTimeout(resolve, 1000)) // 1 second timeout
|
||||
]).catch(error => {
|
||||
this.logger.error('Error stopping recording during unload', error);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const resolveElectronRenderer = (): ElectronRenderer | null => {
|
|||
if (windowElectron) {
|
||||
return windowElectron ?? null;
|
||||
}
|
||||
return (globalThis as ElectronGlobal).electron ?? null;
|
||||
return (window as ElectronGlobal).electron ?? null;
|
||||
};
|
||||
|
||||
export class SafeStorageService {
|
||||
|
|
@ -175,6 +175,14 @@ export class SafeStorageService {
|
|||
return '';
|
||||
}
|
||||
|
||||
private static encodeBinaryString(text: string): string {
|
||||
return Buffer.from(text, 'latin1').toString('base64');
|
||||
}
|
||||
|
||||
private static decodeBinaryString(encoded: string): string {
|
||||
return Buffer.from(encoded, 'base64').toString('latin1');
|
||||
}
|
||||
|
||||
private static xorEncrypt(text: string, key: string): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
|
|
@ -182,12 +190,12 @@ export class SafeStorageService {
|
|||
text.charCodeAt(i) ^ key.charCodeAt(i % key.length)
|
||||
);
|
||||
}
|
||||
return btoa(result);
|
||||
return this.encodeBinaryString(result);
|
||||
}
|
||||
|
||||
private static xorDecrypt(encoded: string, key: string): string {
|
||||
try {
|
||||
const text = atob(encoded);
|
||||
const text = this.decodeBinaryString(encoded);
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
button.setCta();
|
||||
|
||||
// 3秒後に元に戻す
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
button.setButtonText(this.i18n.t('ui.buttons.connectionTest'));
|
||||
button.removeCta();
|
||||
button.setDisabled(false);
|
||||
|
|
@ -227,7 +227,7 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
button.setButtonText(this.i18n.t('ui.buttons.testFailed'));
|
||||
|
||||
// 3秒後に元に戻す
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
button.setButtonText(this.i18n.t('ui.buttons.connectionTest'));
|
||||
button.setDisabled(false);
|
||||
}, 3000);
|
||||
|
|
@ -262,7 +262,6 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
|
||||
const FVAD_DOWNLOAD_URL = 'https://github.com/echogarden-project/fvad-wasm';
|
||||
const wasmFileName = 'fvad.wasm';
|
||||
const loaderFileName = 'fvad.js';
|
||||
const vadInstructionsPath = getLocalVadInstructionsPath(this.app);
|
||||
const initialVadMode = this.plugin.settings.vadMode ?? 'disabled';
|
||||
const vadModeSetting = new Setting(containerEl)
|
||||
|
|
@ -299,21 +298,19 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
helperNote = helperContainer.createDiv({ cls: 'setting-item-description' });
|
||||
const buttonElement = helperContainer.createEl('button', { text: this.i18n.t('ui.settings.vadModeInstallButton') });
|
||||
|
||||
if (buttonElement instanceof HTMLButtonElement) {
|
||||
if (buttonElement.instanceOf(HTMLButtonElement)) {
|
||||
helperButton = buttonElement;
|
||||
helperButton.classList.add('mod-cta');
|
||||
helperContainer.classList.add('voice-input-hidden');
|
||||
|
||||
helperButton.addEventListener('click', () => {
|
||||
const input = document.createElement('input');
|
||||
const input = activeDocument.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.wasm,.js,application/wasm';
|
||||
input.multiple = true;
|
||||
input.accept = '.wasm,application/wasm';
|
||||
input.onchange = () => {
|
||||
this.runAsync(async () => {
|
||||
const files = input.files ? Array.from(input.files) : [];
|
||||
const wasmFile = files.find(file => file.name === wasmFileName);
|
||||
const jsFile = files.find(file => file.name === loaderFileName);
|
||||
|
||||
if (!wasmFile) {
|
||||
new Notice(this.i18n.t('ui.settings.vadModeInstallInvalidName'));
|
||||
|
|
@ -342,20 +339,7 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
|
||||
const wasmTarget = getLocalVadAssetPath(this.app, wasmFileName);
|
||||
await adapter.writeBinary(wasmTarget, toArrayBuffer(wasmBytes));
|
||||
|
||||
let loaderPresent = await adapter.exists(getLocalVadAssetPath(this.app, loaderFileName));
|
||||
if (jsFile) {
|
||||
const loaderContent = await jsFile.text();
|
||||
const loaderTarget = getLocalVadAssetPath(this.app, loaderFileName);
|
||||
await adapter.write(loaderTarget, loaderContent);
|
||||
loaderPresent = true;
|
||||
}
|
||||
|
||||
if (!loaderPresent) {
|
||||
new Notice(this.i18n.t('ui.settings.vadModeInstallJsMissing'));
|
||||
} else {
|
||||
new Notice(this.i18n.t('ui.settings.vadModeInstallSuccess'));
|
||||
}
|
||||
new Notice(this.i18n.t('ui.settings.vadModeInstallSuccess'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice(this.i18n.t('notification.error.fileWrite'));
|
||||
|
|
@ -375,27 +359,27 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
const createVadDescription = (includeMissing: boolean, includeLocal: boolean): DocumentFragment => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const fragment = activeDocument.createDocumentFragment();
|
||||
fragment.appendText(this.i18n.t('ui.settings.vadModeDesc'));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendText(`${this.i18n.t('ui.options.vadServer')}: ${this.i18n.t('ui.settings.vadModeSummaryServer')}`);
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendText(`${this.i18n.t('ui.options.vadLocal')}: ${this.i18n.t('ui.settings.vadModeSummaryLocal')}`);
|
||||
|
||||
if (includeMissing) {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendText(this.i18n.t('ui.settings.vadModeLocalMissing', { path: vadInstructionsPath }));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
const link = document.createElement('a');
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
const link = activeDocument.createElement('a');
|
||||
link.href = FVAD_DOWNLOAD_URL;
|
||||
link.textContent = this.i18n.t('ui.settings.vadModeInstallLinkLabel');
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
fragment.appendChild(link);
|
||||
} else if (includeLocal) {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendChild(activeDocument.createElement('br'));
|
||||
fragment.appendText(this.i18n.t('ui.settings.vadModeLocalAvailable', { path: vadInstructionsPath }));
|
||||
}
|
||||
|
||||
|
|
@ -743,17 +727,17 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
const a = activeDocument.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `voice-input-dictionary-${new Date().toISOString().split('T')[0]}.json`;
|
||||
|
||||
// Add link to document temporarily
|
||||
document.body.appendChild(a);
|
||||
activeDocument.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
window.setTimeout(() => {
|
||||
activeDocument.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
|
||||
|
|
@ -769,7 +753,7 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
* Import dictionary from JSON
|
||||
*/
|
||||
private importDictionary() {
|
||||
const input = document.createElement('input');
|
||||
const input = activeDocument.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
|
|
|
|||
|
|
@ -388,11 +388,11 @@
|
|||
|
||||
/* Utility classes for JavaScript style replacements */
|
||||
.voice-input-hidden {
|
||||
display: none !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-input-flex {
|
||||
display: flex !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Audio Visualizer Styles */
|
||||
|
|
@ -450,6 +450,6 @@
|
|||
|
||||
/* Form element styles */
|
||||
.voice-input-textarea-wide {
|
||||
width: 100% !important;
|
||||
min-height: 100px !important;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
|
|
|||
19
src/types/fvad-wasm.d.ts
vendored
Normal file
19
src/types/fvad-wasm.d.ts
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
declare module '@echogarden/fvad-wasm' {
|
||||
interface FvadWasmModule {
|
||||
_fvad_new(): number;
|
||||
_fvad_free(handle: number): void;
|
||||
_fvad_set_mode(handle: number, mode: number): number;
|
||||
_fvad_set_sample_rate(handle: number, sampleRate: number): number;
|
||||
_fvad_process(handle: number, audioPtr: number, frameSize: number): number;
|
||||
_malloc(size: number): number;
|
||||
_free(ptr: number): void;
|
||||
HEAP16: { buffer: ArrayBuffer };
|
||||
}
|
||||
|
||||
type FvadWasmModuleFactory = (options?: {
|
||||
wasmBinary?: Uint8Array;
|
||||
}) => Promise<FvadWasmModule>;
|
||||
|
||||
const createFvadModule: FvadWasmModuleFactory;
|
||||
export default createFvadModule;
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { App, normalizePath } from 'obsidian';
|
|||
import { FILE_CONSTANTS } from '../config';
|
||||
|
||||
const FVAD_WASM = 'fvad.wasm';
|
||||
const FVAD_JS = 'fvad.js';
|
||||
|
||||
function getPluginBasePath(app: App): string {
|
||||
return normalizePath(`${app.vault.configDir}/plugins/${FILE_CONSTANTS.PLUGIN_ID}`);
|
||||
|
|
@ -20,15 +19,8 @@ export async function hasLocalVadAssets(app: App): Promise<boolean> {
|
|||
getPluginAssetPath(app, FVAD_WASM),
|
||||
normalizePath(`${baseDir}/node_modules/@echogarden/fvad-wasm/${FVAD_WASM}`)
|
||||
];
|
||||
const jsCandidates = [
|
||||
getPluginAssetPath(app, FVAD_JS),
|
||||
normalizePath(`${baseDir}/node_modules/@echogarden/fvad-wasm/${FVAD_JS}`)
|
||||
];
|
||||
|
||||
const hasWasm = await existsInCandidates(adapter, wasmCandidates);
|
||||
const hasJs = await existsInCandidates(adapter, jsCandidates);
|
||||
|
||||
return hasWasm && hasJs;
|
||||
return existsInCandidates(adapter, wasmCandidates);
|
||||
}
|
||||
|
||||
export function getLocalVadInstructionsPath(app: App): string {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export class VoiceInputView extends ItemView {
|
|||
ui: VoiceInputViewUI;
|
||||
actions: VoiceInputViewActions;
|
||||
private blurHandler: ((e: FocusEvent) => void) | null = null;
|
||||
private autoSaveTimeout: NodeJS.Timeout | null = null;
|
||||
private periodicSaveInterval: NodeJS.Timeout | null = null;
|
||||
private autoSaveTimeout: number | null = null;
|
||||
private periodicSaveInterval: number | null = null;
|
||||
private static readonly AUTO_SAVE_DELAY = 2000; // 2 seconds
|
||||
private static readonly PERIODIC_SAVE_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ export class VoiceInputView extends ItemView {
|
|||
|
||||
private async handleOpen(): Promise<void> {
|
||||
const containerElement = this.containerEl.children[1];
|
||||
if (containerElement instanceof HTMLElement) {
|
||||
if (containerElement.instanceOf(HTMLElement)) {
|
||||
const container = containerElement;
|
||||
container.empty();
|
||||
container.addClass('voice-input-view');
|
||||
|
|
@ -99,13 +99,13 @@ export class VoiceInputView extends ItemView {
|
|||
private async handleClose(): Promise<void> {
|
||||
// Clear any pending auto-save timeout
|
||||
if (this.autoSaveTimeout) {
|
||||
clearTimeout(this.autoSaveTimeout);
|
||||
window.clearTimeout(this.autoSaveTimeout);
|
||||
this.autoSaveTimeout = null;
|
||||
}
|
||||
|
||||
// Clear periodic save interval
|
||||
if (this.periodicSaveInterval) {
|
||||
clearInterval(this.periodicSaveInterval);
|
||||
window.clearInterval(this.periodicSaveInterval);
|
||||
this.periodicSaveInterval = null;
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ export class VoiceInputView extends ItemView {
|
|||
refreshUI() {
|
||||
// Re-create the UI with new language
|
||||
const containerElement = this.containerEl.children[1];
|
||||
if (containerElement instanceof HTMLElement) {
|
||||
if (containerElement.instanceOf(HTMLElement)) {
|
||||
const container = containerElement;
|
||||
container.empty();
|
||||
container.addClass('voice-input-view');
|
||||
|
|
@ -234,11 +234,11 @@ export class VoiceInputView extends ItemView {
|
|||
private setupPeriodicSave() {
|
||||
// Clear existing interval if any
|
||||
if (this.periodicSaveInterval) {
|
||||
clearInterval(this.periodicSaveInterval);
|
||||
window.clearInterval(this.periodicSaveInterval);
|
||||
}
|
||||
|
||||
// Set up periodic save every 5 seconds
|
||||
this.periodicSaveInterval = setInterval(() => {
|
||||
this.periodicSaveInterval = window.setInterval(() => {
|
||||
if (this.ui?.textArea) {
|
||||
const content = this.ui.textArea.value;
|
||||
if (content.trim()) {
|
||||
|
|
@ -258,11 +258,11 @@ export class VoiceInputView extends ItemView {
|
|||
onTextChanged() {
|
||||
// Clear existing timeout
|
||||
if (this.autoSaveTimeout) {
|
||||
clearTimeout(this.autoSaveTimeout);
|
||||
window.clearTimeout(this.autoSaveTimeout);
|
||||
}
|
||||
|
||||
// Set new timeout for auto-save (for responsiveness, keeps existing behavior)
|
||||
this.autoSaveTimeout = setTimeout(() => {
|
||||
this.autoSaveTimeout = window.setTimeout(() => {
|
||||
if (this.ui?.textArea) {
|
||||
const content = this.ui.textArea.value;
|
||||
if (content.trim()) {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ export class VoiceInputViewActions {
|
|||
private isProcessingAudio = false;
|
||||
// 連打や高速操作による並行実行を防ぐための遷移ロック
|
||||
private isTransitioning = false;
|
||||
private statusTimer: NodeJS.Timeout | null = null;
|
||||
private clearConfirmTimer: NodeJS.Timeout | null = null;
|
||||
private statusTimer: number | null = null;
|
||||
private clearConfirmTimer: number | null = null;
|
||||
private clearPressCount = 0;
|
||||
private pendingVadAutoStop = false;
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ export class VoiceInputViewActions {
|
|||
|
||||
// Clear existing timer
|
||||
if (this.statusTimer) {
|
||||
clearTimeout(this.statusTimer);
|
||||
window.clearTimeout(this.statusTimer);
|
||||
this.statusTimer = null;
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +373,7 @@ export class VoiceInputViewActions {
|
|||
this.view.ui.statusEl.removeClass('error');
|
||||
|
||||
// Set timer to clear status
|
||||
this.statusTimer = setTimeout(() => {
|
||||
this.statusTimer = window.setTimeout(() => {
|
||||
if (this.view.ui.statusEl && !this.recordingState.isRecording && !this.isProcessingAudio) {
|
||||
this.view.ui.statusEl.setText(this.i18n.t('status.idle'));
|
||||
}
|
||||
|
|
@ -459,7 +459,7 @@ export class VoiceInputViewActions {
|
|||
// Check if new items were added while processing
|
||||
if (this.recordingState.processingQueue.length > 0) {
|
||||
// Use setTimeout to avoid potential stack overflow
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
void this.processQueue().catch((error) => {
|
||||
this.logger.error('Failed to continue processing audio queue', error);
|
||||
});
|
||||
|
|
@ -609,14 +609,14 @@ export class VoiceInputViewActions {
|
|||
this.setStatusWithTimeout(this.i18n.t('status.warning.clearConfirm'));
|
||||
|
||||
// Set timer to reset press count
|
||||
this.clearConfirmTimer = setTimeout(() => {
|
||||
this.clearConfirmTimer = window.setTimeout(() => {
|
||||
this.clearPressCount = 0;
|
||||
this.clearConfirmTimer = null;
|
||||
}, 3000);
|
||||
} else if (this.clearPressCount === 2) {
|
||||
// Second press within timeout - clear the text
|
||||
if (this.clearConfirmTimer) {
|
||||
clearTimeout(this.clearConfirmTimer);
|
||||
window.clearTimeout(this.clearConfirmTimer);
|
||||
this.clearConfirmTimer = null;
|
||||
}
|
||||
this.clearPressCount = 0;
|
||||
|
|
@ -891,13 +891,13 @@ export class VoiceInputViewActions {
|
|||
|
||||
// Clear status timer
|
||||
if (this.statusTimer) {
|
||||
clearTimeout(this.statusTimer);
|
||||
window.clearTimeout(this.statusTimer);
|
||||
this.statusTimer = null;
|
||||
}
|
||||
|
||||
// Clear clear confirm timer
|
||||
if (this.clearConfirmTimer) {
|
||||
clearTimeout(this.clearConfirmTimer);
|
||||
window.clearTimeout(this.clearConfirmTimer);
|
||||
this.clearConfirmTimer = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -194,13 +194,13 @@ export class VoiceInputViewUI {
|
|||
this.setupRecordButtonHandlers();
|
||||
|
||||
// Legacy buttons (hidden)
|
||||
this.formatButton = document.createElement('button');
|
||||
this.formatButton = activeDocument.createElement('button');
|
||||
this.formatButton.classList.add('voice-input-hidden');
|
||||
|
||||
this.contextCorrectionButton = document.createElement('button');
|
||||
this.contextCorrectionButton = activeDocument.createElement('button');
|
||||
this.contextCorrectionButton.classList.add('voice-input-hidden');
|
||||
|
||||
this.cleanupButton = document.createElement('button');
|
||||
this.cleanupButton = activeDocument.createElement('button');
|
||||
this.cleanupButton.classList.add('voice-input-hidden');
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +255,7 @@ export class VoiceInputViewUI {
|
|||
* Setup record button event handlers for both click and push-to-talk
|
||||
*/
|
||||
private setupRecordButtonHandlers() {
|
||||
let longPressTimer: NodeJS.Timeout | null = null;
|
||||
let longPressTimer: number | null = null;
|
||||
let isPushToTalk = false;
|
||||
// let pressStartTime = 0;
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ export class VoiceInputViewUI {
|
|||
// pressStartTime = Date.now();
|
||||
|
||||
// Start a timer to begin recording after threshold
|
||||
longPressTimer = setTimeout(() => {
|
||||
longPressTimer = window.setTimeout(() => {
|
||||
isPushToTalk = true;
|
||||
longPressTimer = null;
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ export class VoiceInputViewUI {
|
|||
|
||||
// Cancel timer if still running (short press)
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
window.clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
return;
|
||||
}
|
||||
|
|
@ -307,7 +307,7 @@ export class VoiceInputViewUI {
|
|||
console.error('Failed to stop recording after push-to-talk', error);
|
||||
});
|
||||
// Keep isPushToTalk true to prevent click handler
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
isPushToTalk = false;
|
||||
}, UI_CONSTANTS.PUSH_TO_TALK_RESET_DELAY);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ export class App {
|
|||
vault = new Vault();
|
||||
}
|
||||
|
||||
export const Platform = {
|
||||
isMobileApp: false,
|
||||
isDesktopApp: true,
|
||||
};
|
||||
|
||||
export class Plugin {
|
||||
app = new App();
|
||||
|
||||
|
|
|
|||
11
tests/unit/security/safe-storage-service.test.ts
Normal file
11
tests/unit/security/safe-storage-service.test.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { SafeStorageService } from '../../../src/security/SafeStorageService';
|
||||
|
||||
describe('SafeStorageService', () => {
|
||||
test('round-trips API keys through the fallback store format without browser base64 APIs', () => {
|
||||
const apiKey = 'sk-proj-test-key-abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
|
||||
const stored = SafeStorageService.encryptForStore(apiKey);
|
||||
expect(stored).toMatch(/^XOR_V1::/u);
|
||||
expect(SafeStorageService.decryptFromStore(stored)).toBe(apiKey);
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"types": ["node"],
|
||||
"lib": ["DOM", "ES2018", "DOM.Iterable"]
|
||||
},
|
||||
"include": ["**/*.ts", "src/types/**/*.d.ts"],
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
"0.9.0": "1.8.0",
|
||||
"0.9.1": "1.8.0",
|
||||
"0.9.2": "1.8.0",
|
||||
"0.9.3": "1.8.0"
|
||||
"0.9.3": "1.8.0",
|
||||
"0.9.4": "1.8.0",
|
||||
"0.9.5": "1.8.0"
|
||||
}
|
||||
Loading…
Reference in a new issue