mirror of
https://github.com/mssoftjp/obsidian-voice-input.git
synced 2026-07-22 06:44:48 +00:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cde86540e |
20 changed files with 108 additions and 187 deletions
16
README.md
16
README.md
|
|
@ -13,7 +13,7 @@ Capture notes with high-accuracy multilingual voice input using OpenAI Speech-to
|
|||
- 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
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ Compatibility: requires Obsidian 1.8.0 or later (per `minAppVersion`).
|
|||
|
||||
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 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 from the installed plugin folder. No external network requests are made for local VAD. Local VAD is desktop‑only.
|
||||
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
|
||||
|
||||
|
|
@ -60,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
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ Tip: A settings gear in the view header opens the plugin’s settings.
|
|||
- 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` and `fvad.js` from the plugin folder after you install or choose those files. The release bundle does not include these WebAssembly files.
|
||||
- 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.
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
- ビュー内のクイック操作(コピー/クリア/カーソル位置へ挿入/末尾へ追記)
|
||||
- 自動保存(定期保存とフォーカス外れ時)。再オープン時に自動復元
|
||||
- 多言語サポート(日本語、英語、中国語、韓国語のインターフェース)
|
||||
- VADモード選択(標準はオフ=精度重視。必要に応じてサーバーVAD(応答を速めたい場合)や `fvad.wasm` / `fvad.js` を使ったローカルVAD(デスクトップの自動停止)を利用可能)
|
||||
- VADモード選択(標準はオフ=精度重視。必要に応じてサーバーVAD(応答を速めたい場合)や `fvad.wasm` を使ったローカルVAD(デスクトップの自動停止)を利用可能)
|
||||
|
||||
## 必要条件
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
|
||||
リリースアセット: プラグインの実行に必要なファイルはリリースバンドル内の `main.js`、`manifest.json`、`styles.css` です。任意のローカルVADファイルはユーザーが用意するもので、リリースには含めません。
|
||||
|
||||
ローカルVAD(任意): 本プラグインは WebAssembly ファイルを同梱しません。ローカルVADの自動停止を使う場合、`fvad.wasm` と `fvad.js` を fvad‑wasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm / fvad.js を選択…」ボタンでコピーしてください。ローダーはインストール済みプラグインフォルダ内のローカルファイルを `fetch` で読むだけで、ローカルVADのための外部ネットワーク通信は行いません。ローカルVADはデスクトップ専用です。
|
||||
ローカルVAD(任意): 本プラグインは WebAssembly ファイルを同梱しません。ローカルVADの自動停止を使う場合、`fvad.wasm` を fvad‑wasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm を選択…」ボタンでコピーしてください。プラグインはインストール済みプラグインフォルダ内のこのローカルファイルを読み込みます。ローカルVADのための外部ネットワーク通信は行いません。ローカルVADはデスクトップ専用です。
|
||||
|
||||
## コマンド
|
||||
|
||||
|
|
@ -150,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` を手動導入)から選択
|
||||
|
||||
### ドラフトと同期
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ Third‑party licensing: see `THIRD_PARTY_LICENSES.md`.
|
|||
- 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` と `fvad.js` をプラグインフォルダから読み込みます。リリースバンドルにはこれらの WebAssembly ファイルは含めません。
|
||||
- ローカルファイル: 任意のローカル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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "voice-input",
|
||||
"name": "Voice Input",
|
||||
"version": "0.9.4",
|
||||
"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",
|
||||
|
|
|
|||
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -1,16 +1,15 @@
|
|||
{
|
||||
"name": "voice-input",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "voice-input",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@echogarden/fvad-wasm": "^0.2.0",
|
||||
"lodash.merge": "^4.6.2"
|
||||
"@echogarden/fvad-wasm": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.2",
|
||||
|
|
@ -19,7 +18,6 @@
|
|||
"@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.3.0",
|
||||
|
|
@ -3664,19 +3662,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
|
|
@ -8571,6 +8556,7 @@
|
|||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "voice-input",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"description": "Voice input plugin for Obsidian with accurate transcription",
|
||||
"main": "main.js",
|
||||
"engines": {
|
||||
|
|
@ -40,7 +40,6 @@
|
|||
"@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.3.0",
|
||||
|
|
@ -54,7 +53,6 @@
|
|||
"typescript-eslint": "^8.46.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@echogarden/fvad-wasm": "^0.2.0",
|
||||
"lodash.merge": "^4.6.2"
|
||||
"@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;
|
||||
|
|
|
|||
|
|
@ -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 = activeDocument.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'));
|
||||
};
|
||||
|
||||
// スクリプトを実行
|
||||
activeDocument.head.appendChild(script);
|
||||
|
||||
// モジュールが読み込まれるのを待つ
|
||||
window.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 {};
|
||||
}
|
||||
});
|
||||
|
||||
// クリーンアップ
|
||||
activeDocument.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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -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 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 and 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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -307,13 +306,11 @@ export class VoiceInputSettingTab extends PluginSettingTab {
|
|||
helperButton.addEventListener('click', () => {
|
||||
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'));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
"0.9.1": "1.8.0",
|
||||
"0.9.2": "1.8.0",
|
||||
"0.9.3": "1.8.0",
|
||||
"0.9.4": "1.8.0"
|
||||
"0.9.4": "1.8.0",
|
||||
"0.9.5": "1.8.0"
|
||||
}
|
||||
Loading…
Reference in a new issue