diff --git a/src/core/audio/AudioRecorder.ts b/src/core/audio/AudioRecorder.ts index 8b1ebf6..f6ab141 100644 --- a/src/core/audio/AudioRecorder.ts +++ b/src/core/audio/AudioRecorder.ts @@ -390,10 +390,11 @@ export class AudioRecorder extends Disposable { return; } - this.processingAnalyserNode.getFloatTimeDomainData(this.analyserDataBuffer); + const analyserBuffer = this.analyserDataBuffer as Float32Array; + this.processingAnalyserNode.getFloatTimeDomainData(analyserBuffer); // Copy the buffer to avoid mutation before processing completes - const audioData = new Float32Array(this.analyserDataBuffer); + const audioData = new Float32Array(analyserBuffer); try { this.processAudioData(audioData); } catch (error) { diff --git a/src/core/audio/AudioVisualizer.ts b/src/core/audio/AudioVisualizer.ts index ccc22b1..a37e037 100644 --- a/src/core/audio/AudioVisualizer.ts +++ b/src/core/audio/AudioVisualizer.ts @@ -68,15 +68,16 @@ export class AudioVisualizer extends Disposable { this.animationId = requestAnimationFrame(() => this.draw()); // Get time domain data (waveform) - this.analyser.getByteTimeDomainData(this.dataArray); + const waveformArray = this.dataArray as Uint8Array; + this.analyser.getByteTimeDomainData(waveformArray); // Calculate RMS (Root Mean Square) for current audio level let sum = 0; - for (let i = 0; i < this.dataArray.length; i++) { - const normalized = (this.dataArray[i] - 128) / 128; + for (let i = 0; i < waveformArray.length; i++) { + const normalized = (waveformArray[i] - 128) / 128; sum += normalized * normalized; } - const rms = Math.sqrt(sum / this.dataArray.length); + const rms = Math.sqrt(sum / waveformArray.length); this.currentLevel = rms; // Apply scaling factor to make waveform more visible @@ -285,13 +286,14 @@ export class SimpleAudioLevelIndicator extends Disposable { this.animationId = requestAnimationFrame(() => this.update()); - this.analyser.getByteFrequencyData(this.dataArray); + const frequencyArray = this.dataArray as Uint8Array; + this.analyser.getByteFrequencyData(frequencyArray); // Calculate average level with emphasis on lower frequencies (voice range) let sum = 0; - const relevantBins = Math.min(this.dataArray.length / 4, 50); // Focus on voice frequencies + const relevantBins = Math.min(frequencyArray.length / 4, 50); // Focus on voice frequencies for (let i = 0; i < relevantBins; i++) { - sum += this.dataArray[i]; + sum += frequencyArray[i]; } const average = sum / relevantBins; // Apply scaling for better visibility diff --git a/src/plugin/VoiceInputPlugin.ts b/src/plugin/VoiceInputPlugin.ts index b3d1dfc..f67d207 100644 --- a/src/plugin/VoiceInputPlugin.ts +++ b/src/plugin/VoiceInputPlugin.ts @@ -1,7 +1,7 @@ import { Plugin } from 'obsidian'; -import { VoiceInputSettings, DEFAULT_SETTINGS } from '../interfaces'; +import { VoiceInputSettings, DEFAULT_SETTINGS, Locale, SUPPORTED_LOCALES } from '../interfaces'; import { VoiceInputView, VIEW_TYPE_VOICE_INPUT } from '../views'; import { VoiceInputSettingTab } from '../settings'; import { ViewManager, DraftManager } from '../managers'; @@ -11,7 +11,12 @@ import { serviceLocator, ServiceKeys, getI18nService } from '../services'; import { getObsidianLocale } from '../types'; import { SafeStorageService } from '../security'; -type LegacyVoiceInputSettings = Partial & { +type LegacyAdvancedSettings = Partial> & { + transcriptionLanguage?: string; +}; + +type LegacyVoiceInputSettings = Partial> & { + advanced?: LegacyAdvancedSettings; language?: string; interfaceLanguage?: string; recordingMode?: string; @@ -19,6 +24,10 @@ type LegacyVoiceInputSettings = Partial & { minSpeechDuration?: number; }; +const isSupportedLocale = (value: unknown): value is Locale => { + return typeof value === 'string' && (SUPPORTED_LOCALES as readonly string[]).includes(value as Locale); +}; + export default class VoiceInputPlugin extends Plugin { settings: VoiceInputSettings; private viewManager: ViewManager; @@ -244,7 +253,10 @@ export default class VoiceInputPlugin extends Plugin { // interfaceLanguageからpluginLanguageへの移行 if ('interfaceLanguage' in data && !('pluginLanguage' in data)) { - migratedData.pluginLanguage = data.interfaceLanguage; + const interfaceLanguage = data.interfaceLanguage; + migratedData.pluginLanguage = isSupportedLocale(interfaceLanguage) + ? interfaceLanguage + : this.detectPluginLanguage(); delete migratedData.interfaceLanguage; needsSave = true; this.logger?.info('Migrating interfaceLanguage to pluginLanguage'); @@ -254,7 +266,7 @@ export default class VoiceInputPlugin extends Plugin { if ('language' in data && !('transcriptionLanguage' in data)) { // 既存のlanguageフィールドをtranscriptionLanguageに移行 const langValue = data.language; - if (langValue === 'ja' || langValue === 'en' || langValue === 'zh' || langValue === 'ko') { + if (isSupportedLocale(langValue)) { migratedData.transcriptionLanguage = langValue; } else if (langValue === 'auto') { // auto は廃止: 起動環境のロケールへ固定 @@ -270,8 +282,8 @@ export default class VoiceInputPlugin extends Plugin { // languageからpluginLanguageへの移行(古いバージョンとの互換性のため) if ('language' in data && !('pluginLanguage' in data)) { // 言語コードを正規化(ja → ja、en → en、zh → zh、ko → ko、その他 → en) - const langCode = data.language as string; - if (langCode === 'ja' || langCode === 'en' || langCode === 'zh' || langCode === 'ko') { + const langCode = data.language; + if (isSupportedLocale(langCode)) { migratedData.pluginLanguage = langCode; } else { migratedData.pluginLanguage = 'en'; @@ -306,7 +318,7 @@ export default class VoiceInputPlugin extends Plugin { } // 保存されたデータをデフォルト設定に上書き(undefinedの値は除外) - mergeSettings(this.settings, migratedData); + mergeSettings(this.settings, migratedData as Partial); // APIキーの復号化 if (this.settings.openaiApiKey) { @@ -363,14 +375,13 @@ export default class VoiceInputPlugin extends Plugin { }; needsSave = true; this.logger?.info('Initialized advanced settings with language linking enabled for backward compatibility'); - } else if (data.advanced && !hasSettingsKey(data.advanced, 'languageLinkingEnabled')) { - // advancedオブジェクトは存在するが、languageLinkingEnabledが無い場合 - this.settings.advanced.languageLinkingEnabled = true; - needsSave = true; - this.logger?.info('Added languageLinkingEnabled to existing advanced settings'); } else if (data.advanced) { - // auto からの置換 - const adv = data.advanced as { transcriptionLanguage?: string }; + const adv = data.advanced as LegacyAdvancedSettings; + if (typeof adv.languageLinkingEnabled === 'undefined') { + this.settings.advanced.languageLinkingEnabled = true; + needsSave = true; + this.logger?.info('Added languageLinkingEnabled to existing advanced settings'); + } if (adv.transcriptionLanguage === 'auto') { this.settings.advanced.transcriptionLanguage = this.detectPluginLanguage(); needsSave = true; diff --git a/src/security/SafeStorageService.ts b/src/security/SafeStorageService.ts index 981dd2e..f34d09e 100644 --- a/src/security/SafeStorageService.ts +++ b/src/security/SafeStorageService.ts @@ -35,11 +35,19 @@ const isElectronWindow = (win: Window): win is ElectronWindow => { }; const resolveElectronRenderer = (): ElectronRenderer | null => { - if (isElectronWindow(window) && typeof (window as ElectronWindow).require === 'function') { - return (window as ElectronWindow).require('electron') ?? null; + if (isElectronWindow(window)) { + const electronWindow = window as ElectronWindow; + const requireFn = electronWindow.require; + if (typeof requireFn === 'function') { + return requireFn('electron') ?? null; + } + if (electronWindow.electron) { + return electronWindow.electron ?? null; + } } - if ((window as ElectronWindow).electron) { - return (window as ElectronWindow).electron ?? null; + const windowElectron = (window as ElectronWindow).electron; + if (windowElectron) { + return windowElectron ?? null; } return (globalThis as ElectronGlobal).electron ?? null; }; diff --git a/src/settings/VoiceInputSettingTab.ts b/src/settings/VoiceInputSettingTab.ts index 07b0d91..ba51024 100644 --- a/src/settings/VoiceInputSettingTab.ts +++ b/src/settings/VoiceInputSettingTab.ts @@ -28,6 +28,17 @@ const isTranscriptionModel = (value: string): value is TranscriptionModelOption const isVadMode = (value: string): value is VadModeOption => value === 'server' || value === 'local' || value === 'disabled'; +const toArrayBuffer = (view: Uint8Array): ArrayBuffer => { + const { buffer, byteOffset, byteLength } = view; + if (buffer instanceof ArrayBuffer) { + return byteOffset === 0 && byteLength === buffer.byteLength + ? buffer + : buffer.slice(byteOffset, byteOffset + byteLength); + } + const clone = Uint8Array.from(view); + return clone.buffer instanceof ArrayBuffer ? clone.buffer : new ArrayBuffer(0); +}; + interface DictionaryExportData { version: string; definiteCorrections: CorrectionEntry[]; @@ -335,7 +346,7 @@ export class VoiceInputSettingTab extends PluginSettingTab { } const wasmTarget = getLocalVadAssetPath(this.app, wasmFileName); - await adapter.writeBinary(wasmTarget, wasmBytes); + await adapter.writeBinary(wasmTarget, toArrayBuffer(wasmBytes)); let loaderPresent = await adapter.exists(getLocalVadAssetPath(this.app, loaderFileName)); if (jsFile) { diff --git a/src/views/VoiceInputView.ts b/src/views/VoiceInputView.ts index c04a24c..394c16f 100644 --- a/src/views/VoiceInputView.ts +++ b/src/views/VoiceInputView.ts @@ -1,4 +1,5 @@ import { + Events, ItemView, WorkspaceLeaf } from 'obsidian'; @@ -41,10 +42,12 @@ export class VoiceInputView extends ItemView { return 'microphone'; } - onOpen(): void { - void this.handleOpen().catch((error) => { + async onOpen(): Promise { + try { + await this.handleOpen(); + } catch (error) { console.error('Failed to open Voice Input view', error); - }); + } } private async handleOpen(): Promise { @@ -75,7 +78,8 @@ export class VoiceInputView extends ItemView { this.setupPeriodicSave(); // Subscribe to settings change events to keep UI toggles in sync - const eventRef = this.app.workspace.on('voice-input:settings-changed', () => { + const workspaceEvents = this.app.workspace as Events; + const eventRef = workspaceEvents.on('voice-input:settings-changed', () => { this.ui?.updateSettingsUI(); this.actions?.updateTranscriptionService(); }); @@ -84,10 +88,12 @@ export class VoiceInputView extends ItemView { } } - onClose(): void { - void this.handleClose().catch((error) => { + async onClose(): Promise { + try { + await this.handleClose(); + } catch (error) { console.error('Failed to close Voice Input view', error); - }); + } } private async handleClose(): Promise { diff --git a/tsconfig.json b/tsconfig.json index 0dd8329..f6640d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,14 +4,14 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2018", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "ES2015.Iterable", "DOM.Iterable"] + "lib": ["DOM", "ES2018", "DOM.Iterable"] }, "include": ["**/*.ts", "src/types/**/*.d.ts"], "exclude": [ @@ -20,4 +20,4 @@ "build/**", "tests/**" ] -} \ No newline at end of file +}