chore: improve scorecard compliance checks

This commit is contained in:
mssoftjp 2026-05-17 21:14:15 +09:00
parent b4dcb897d2
commit 5c04d21985
16 changed files with 2517 additions and 629 deletions

30
.github/workflows/ci.yml vendored Normal file
View 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@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Run checks
run: npm run check:prepr

View file

@ -10,7 +10,7 @@ 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 postprocessing: 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
- Autosave 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 autostop (requires fvad.wasm/fvad.js, installed manually)
@ -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 autostop, download `fvad.wasm` and `fvad.js` from the fvadwasm 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 desktoponly.
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 autostop, download `fvad.wasm` and `fvad.js` from the fvadwasm 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 desktoponly.
## 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 pushtotalk: longpress 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 plugins settings.
@ -67,13 +69,16 @@ Tip: A settings gear in the view header opens the plugins 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 Obsidians `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 desktoponly by design.
See also OpenAIs 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 Obsidians `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` and `fvad.js` from the plugin folder after you install or choose those files. The release bundle does not include these WebAssembly files.
- 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 [OpenAIs Privacy Policy](https://openai.com/policies/privacy-policy/) for OpenAI API data handling.
## Troubleshooting
@ -114,10 +119,11 @@ Thirdparty 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` を fvadwasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm / fvad.js を選択…」ボタンでコピーしてください。ローカルVADはデスクトップ専用です。
リリースアセット: プラグインの実行に必要なファイルはリリースバンドル内の `main.js`、`manifest.json`、`styles.css` です。任意のローカルVADファイルはユーザーが用意するもので、リリースには含めません。
ローカルVAD任意: 本プラグインは WebAssembly ファイルを同梱しません。ローカルVADの自動停止を使う場合、`fvad.wasm` と `fvad.js` を fvadwasm プロジェクトから取得して同じフォルダに配置するか、設定 → 音声区間検出 の「fvad.wasm / fvad.js を選択…」ボタンでコピーしてください。ローダーはインストール済みプラグインフォルダ内のローカルファイルを `fetch` で読むだけで、ローカルVADのための外部ネットワーク通信は行いません。ローカルVADはデスクトップ専用です。
## コマンド
@ -173,13 +179,16 @@ Thirdparty licensing: see `THIRD_PARTY_LICENSES.md`.
## セキュリティ / プライバシー
- 処理はメモリ内で行い、音声ファイルはプラグイン側でディスク保存しません
- 録音した音声は文字起こしのため OpenAI に送信され、HTTPSObsidian の `requestUrl` 経由)で通信します。
- APIキーは保存時に暗号化
補足: Electron の SafeStorage が利用できない環境では保存キーを軽度に難読化して保持します(本プラグインはデスクトップ専用の設計です)。
OpenAIのプライバシーポリシーもご参照ください。
- 処理はメモリ内で行い、音声ファイルはプラグイン側でディスク保存しません。
- ネットワーク利用: 録音した音声は文字起こしのため `https://api.openai.com/v1/audio/transcriptions` に HTTPSObsidian の `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``fvad.js` をプラグインフォルダから読み込みます。リリースバンドルにはこれらの WebAssembly ファイルは含めません。
- 外部リンク: 設定画面に fvad-wasm の GitHub プロジェクトへのリンクを表示することがありますが、プラグインが自動でこれらのファイルをダウンロードすることはありません。
- プライバシーポリシー: OpenAI API におけるデータの扱いは [OpenAI のプライバシーポリシー](https://openai.com/policies/privacy-policy/) も参照してください。
## トラブルシューティング

View file

@ -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'
}
},

2897
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,9 @@
"version": "0.9.3",
"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,25 @@
"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"
"lodash.merge": "^4.6.2"
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -123,7 +123,7 @@ export class VADProcessor extends Disposable {
// モジュールを評価するための一時的な環境を作成
return new Promise((resolve, reject) => {
// スクリプトタグを作成
const script = document.createElement('script');
const script = activeDocument.createElement('script');
script.type = 'module';
script.textContent =
`const importMeta={url:'file:///${fvadJsPath}'};` +
@ -137,10 +137,10 @@ export class VADProcessor extends Disposable {
};
// スクリプトを実行
document.head.appendChild(script);
activeDocument.head.appendChild(script);
// モジュールが読み込まれるのを待つ
setTimeout(() => {
window.setTimeout(() => {
const loadModule = async (): Promise<void> => {
if (!hasFvadModule(globalWindow)) {
throw new Error('fvad module not found in global scope');
@ -172,7 +172,7 @@ export class VADProcessor extends Disposable {
});
// クリーンアップ
document.head.removeChild(script);
activeDocument.head.removeChild(script);
delete globalWindow.__fvadModule;
};

View file

@ -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));
}
/**

View file

@ -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,7 +180,7 @@ 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.',
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.',
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.',
@ -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'

View file

@ -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);
});

View file

@ -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 {

View file

@ -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);
@ -299,13 +299,13 @@ 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;
@ -375,27 +375,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 +743,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 +769,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';

View file

@ -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()) {

View file

@ -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;
}

View file

@ -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);
}