diff --git a/README.md b/README.md index 06b554b..1d90491 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ High-accuracy voice input for Obsidian. Uses OpenAI GPT-4o Audio Transcriptions - Model selection: GPT‑4o Transcribe or GPT‑4o mini Transcribe - Language separation: independent UI language and voice recognition language settings - Auto language detection: automatic voice recognition language based on Obsidian locale -- AI post‑processing: optional dictionary-based cleanup (Japanese only) +- 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 - Auto‑save drafts: periodic and on blur, automatic restore - Multilingual support: Japanese, English, Chinese, Korean interface languages @@ -51,7 +51,7 @@ Tip: A settings gear in the view header opens the plugin’s settings. - OpenAI API Key: stored locally (encrypted at rest) - Transcription Model: `gpt-4o-transcribe` or `gpt-4o-mini-transcribe` - **Transcription Language**: Auto/Japanese/English/Chinese/Korean (auto-detection recommended) -- AI Post‑processing: enable dictionary‑based cleanup (applied to Japanese only) +- AI Post‑processing: enable dictionary‑based cleanup (applied to all languages when enabled) - Maximum Recording Duration: slider (default 5 min) - Plugin Language: English/Japanese (controls UI display only, auto‑detected from Obsidian, adjustable) diff --git a/src/core/transcription/TranscriptionService.ts b/src/core/transcription/TranscriptionService.ts index fc894d4..4ba070f 100644 --- a/src/core/transcription/TranscriptionService.ts +++ b/src/core/transcription/TranscriptionService.ts @@ -419,6 +419,9 @@ export class TranscriptionService implements ITranscriptionProvider { */ setTranscriptionCorrection(enabled: boolean) { this.enableTranscriptionCorrection = enabled; + this.corrector.updateSettings({ + enabled: enabled + }); } /** @@ -428,9 +431,10 @@ export class TranscriptionService implements ITranscriptionProvider { this.apiKey = apiKey; // API key is no longer needed for the simplified corrector // Preserve existing dictionary settings when updating API key - const currentDict = this.corrector.getSettings().correctionDictionary; + const currentSettings = this.corrector.getSettings(); this.corrector = new DictionaryCorrector({ - correctionDictionary: currentDict + enabled: currentSettings.enabled, + correctionDictionary: currentSettings.correctionDictionary }); } diff --git a/tests/unit/core/transcription/multilingual-dictionary-correction.test.ts b/tests/unit/core/transcription/multilingual-dictionary-correction.test.ts new file mode 100644 index 0000000..ad86158 --- /dev/null +++ b/tests/unit/core/transcription/multilingual-dictionary-correction.test.ts @@ -0,0 +1,214 @@ +/** + * Tests for multilingual dictionary correction functionality + * Validates that dictionary correction applies to all languages when enabled + */ + +import { DictionaryCorrector } from '../../../../src/core/transcription/DictionaryCorrector'; +import { SimpleCorrectionDictionary } from '../../../../src/interfaces'; + +describe('Multilingual Dictionary Correction', () => { + let corrector: DictionaryCorrector; + + // Sample dictionary with common corrections + const testDictionary: SimpleCorrectionDictionary = { + definiteCorrections: [ + { from: ['AI'], to: 'artificial intelligence' }, + { from: ['GPT'], to: 'Generative Pre-trained Transformer' }, + { from: ['API'], to: 'Application Programming Interface' }, + { from: ['UI'], to: 'User Interface' }, + { from: ['あなた'], to: 'あなた様' }, // Japanese correction + { from: ['hello'], to: 'Hello' }, // English correction + { from: ['你好'], to: '您好' }, // Chinese correction (informal to formal) + { from: ['안녕'], to: '안녕하세요' }, // Korean correction (informal to formal) + ] + }; + + beforeEach(() => { + corrector = new DictionaryCorrector({ + enabled: true, + correctionDictionary: testDictionary + }); + }); + + describe('Dictionary correction applies to all languages', () => { + it('should apply corrections to English text', async () => { + const input = 'The AI uses GPT technology for API calls with a modern UI.'; + const result = await corrector.correct(input); + + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('Application Programming Interface'); + expect(result).toContain('User Interface'); + expect(result).not.toContain('AI '); + expect(result).not.toContain('GPT '); + expect(result).not.toContain('API '); + expect(result).not.toContain('UI.'); + }); + + it('should apply corrections to Japanese text', async () => { + const input = 'あなたは AI を使って GPT で API を呼び出します。'; + const result = await corrector.correct(input); + + expect(result).toContain('あなた様'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('Application Programming Interface'); + expect(result).not.toContain('あなたは'); + }); + + it('should apply corrections to Chinese text', async () => { + const input = '你好,AI 使用 GPT 技术通过 API 调用。'; + const result = await corrector.correct(input); + + expect(result).toContain('您好'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('Application Programming Interface'); + expect(result).not.toContain('你好'); + }); + + it('should apply corrections to Korean text', async () => { + const input = '안녕, AI가 GPT 기술로 API를 호출합니다.'; + const result = await corrector.correct(input); + + expect(result).toContain('안녕하세요'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('Application Programming Interface'); + expect(result).not.toContain('안녕,'); + }); + + it('should apply corrections to mixed language text', async () => { + const input = 'Hello, あなた! The AI system uses GPT. 你好 API!'; + const result = await corrector.correct(input); + + expect(result).toContain('Hello'); + expect(result).toContain('あなた様'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('您好'); + expect(result).toContain('Application Programming Interface'); + }); + }); + + describe('Dictionary correction can be disabled', () => { + it('should not apply corrections when disabled', async () => { + const disabledCorrector = new DictionaryCorrector({ + enabled: false, + correctionDictionary: testDictionary + }); + + const input = 'The AI uses GPT technology for API calls with a modern UI.'; + const result = await disabledCorrector.correct(input); + + // Should return original text unchanged + expect(result).toBe(input); + expect(result).toContain('AI'); + expect(result).toContain('GPT'); + expect(result).toContain('API'); + expect(result).toContain('UI'); + }); + }); + + describe('Empty or missing dictionary handling', () => { + it('should handle empty dictionary gracefully', async () => { + const emptyCorrector = new DictionaryCorrector({ + enabled: true, + correctionDictionary: { definiteCorrections: [] } + }); + + const inputs = [ + 'English text with AI and GPT', + 'あなたは日本語です', + '你好中文', + '안녕하세요 한국어' + ]; + + for (const input of inputs) { + const result = await emptyCorrector.correct(input); + expect(result).toBe(input); // Should return unchanged + } + }); + + it('should handle missing dictionary gracefully', async () => { + const noDictCorrector = new DictionaryCorrector({ + enabled: true + // No correctionDictionary provided + }); + + const inputs = [ + 'English text with AI and GPT', + 'あなたは日本語です', + '你好中文', + '안녕하세요 한국어' + ]; + + for (const input of inputs) { + const result = await noDictCorrector.correct(input); + expect(result).toBe(input); // Should return unchanged + } + }); + }); + + describe('Edge cases', () => { + it('should handle short text correctly', async () => { + const shortTexts = ['AI', 'GPT', 'API', 'UI']; + + for (const text of shortTexts) { + const result = await corrector.correct(text); + expect(result).not.toBe(text); // Should be corrected + expect(result.length).toBeGreaterThan(text.length); + } + }); + + it('should handle empty or very short text', async () => { + const emptyTexts = ['', ' ', 'a']; + + for (const text of emptyTexts) { + const result = await corrector.correct(text); + // Should return as-is for very short text based on implementation + expect(result).toBe(text); + } + }); + + it('should handle text without any corrections needed', async () => { + const cleanTexts = [ + 'This is clean English text.', + 'これは綺麗な日本語です。', + '这是干净的中文文本。', + '이것은 깨끗한 한국어 텍스트입니다.' + ]; + + for (const text of cleanTexts) { + const result = await corrector.correct(text); + expect(result).toBe(text); // Should return unchanged + } + }); + }); + + describe('Case sensitivity and pattern matching', () => { + it('should match corrections case-sensitively', async () => { + const input = 'The ai and AI are different, also gpt vs GPT.'; + const result = await corrector.correct(input); + + // Only 'AI' and 'GPT' (exact case matches) should be corrected + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('ai and'); // lowercase 'ai' should remain + expect(result).toContain('gpt vs'); // lowercase 'gpt' should remain + }); + + it('should handle multiple occurrences of the same pattern', async () => { + const input = 'AI helps with AI development. GPT-3 and GPT-4 are AI models.'; + const result = await corrector.correct(input); + + // All instances of 'AI' and 'GPT' should be corrected + expect(result).not.toContain('AI '); + expect(result).not.toContain('GPT-'); + const aiCount = (result.match(/artificial intelligence/g) || []).length; + const gptCount = (result.match(/Generative Pre-trained Transformer/g) || []).length; + expect(aiCount).toBeGreaterThanOrEqual(2); + expect(gptCount).toBeGreaterThanOrEqual(2); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/core/transcription/transcription-service-multilingual-correction.test.ts b/tests/unit/core/transcription/transcription-service-multilingual-correction.test.ts new file mode 100644 index 0000000..7c05bb4 --- /dev/null +++ b/tests/unit/core/transcription/transcription-service-multilingual-correction.test.ts @@ -0,0 +1,187 @@ +/** + * Integration tests for TranscriptionService multilingual dictionary correction + * Validates that the service applies dictionary correction to all languages when enabled + */ + +import { TranscriptionService } from '../../../../src/core/transcription/TranscriptionService'; +import { SimpleCorrectionDictionary } from '../../../../src/interfaces'; + +// Mock the ObsidianHttpClient since we're not testing actual API calls +jest.mock('../../../../src/utils/ObsidianHttpClient', () => ({ + ObsidianHttpClient: { + postFormData: jest.fn() + } +})); + +// Mock the logger +jest.mock('../../../../src/services', () => ({ + createServiceLogger: jest.fn(() => ({ + info: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn() + })) +})); + +describe('TranscriptionService Multilingual Dictionary Correction Integration', () => { + let service: TranscriptionService; + + const testDictionary: SimpleCorrectionDictionary = { + definiteCorrections: [ + { from: ['AI'], to: 'artificial intelligence' }, + { from: ['GPT'], to: 'Generative Pre-trained Transformer' }, + { from: ['こんにちは'], to: 'こんにちは(修正済み)' }, + { from: ['你好'], to: '您好' }, + { from: ['안녕'], to: '안녕하세요' } + ] + }; + + beforeEach(() => { + service = new TranscriptionService('test-api-key', testDictionary); + // Enable dictionary correction + service.setTranscriptionCorrection(true); + }); + + // Helper to verify correction is applied via the service's corrector + describe('Direct corrector access verification', () => { + it('should provide access to corrector with multilingual capability', async () => { + const corrector = service.getCorrector(); + + // Test multiple languages + const testCases = [ + { input: 'This AI uses GPT', expected: 'artificial intelligence' }, + { input: 'こんにちは、AIです', expected: 'こんにちは(修正済み)' }, + { input: '你好,这是AI', expected: '您好' }, + { input: '안녕, AI 시스템', expected: '안녕하세요' } + ]; + + for (const testCase of testCases) { + const result = await corrector.correct(testCase.input); + expect(result).toContain(testCase.expected); + } + }); + + it('should respect the enableTranscriptionCorrection setting', async () => { + const corrector = service.getCorrector(); + + // Test with correction enabled + service.setTranscriptionCorrection(true); + let result = await corrector.correct('This AI system uses GPT'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + + // Test with correction disabled + service.setTranscriptionCorrection(false); + result = await corrector.correct('This AI system uses GPT'); + expect(result).toBe('This AI system uses GPT'); // Should remain unchanged + }); + }); + + describe('Dictionary update functionality', () => { + it('should allow updating dictionary and apply new corrections to all languages', async () => { + const newDictionary: SimpleCorrectionDictionary = { + definiteCorrections: [ + { from: ['ML'], to: 'Machine Learning' }, + { from: ['さようなら'], to: 'さようなら(丁寧)' }, + { from: ['再见'], to: '再見' }, + { from: ['가다'], to: '가시다' } + ] + }; + + // Update dictionary + service.setCustomDictionary(newDictionary); + const corrector = service.getCorrector(); + + // Test that new corrections work for multiple languages + const testCases = [ + { input: 'ML is powerful', expected: 'Machine Learning' }, + { input: 'さようなら、また明日', expected: 'さようなら(丁寧)' }, + { input: '再见朋友', expected: '再見' }, + { input: '가다 오다', expected: '가시다' } + ]; + + for (const testCase of testCases) { + const result = await corrector.correct(testCase.input); + expect(result).toContain(testCase.expected); + } + + // Test that old corrections are replaced + const oldResult = await corrector.correct('This AI uses GPT'); + expect(oldResult).toBe('This AI uses GPT'); // Old corrections should not apply + }); + }); + + describe('Settings update integration', () => { + it('should respect corrector settings updates for all languages', async () => { + const corrector = service.getCorrector(); + + // Initial test - corrections should work + let result = await corrector.correct('AI and GPT systems こんにちは'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('こんにちは(修正済み)'); + + // Update settings to disable corrections + service.updateCorrectorSettings({ enabled: false }); + + // Corrections should no longer apply to any language + result = await corrector.correct('AI and GPT systems こんにちは'); + expect(result).toBe('AI and GPT systems こんにちは'); + + // Re-enable corrections + service.updateCorrectorSettings({ enabled: true }); + + // Corrections should work again for all languages + result = await corrector.correct('AI and GPT systems こんにちは'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + expect(result).toContain('こんにちは(修正済み)'); + }); + }); + + describe('API key update preservation', () => { + it('should preserve dictionary settings when API key is updated', async () => { + const corrector = service.getCorrector(); + + // Test initial corrections work + let result = await corrector.correct('AI and GPT systems'); + expect(result).toContain('artificial intelligence'); + + // Update API key + service.updateApiKey('new-test-api-key'); + + // Dictionary corrections should still work + const newCorrector = service.getCorrector(); + result = await newCorrector.correct('AI and GPT systems'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + }); + }); + + describe('Model update compatibility', () => { + it('should maintain dictionary correction functionality across model changes', async () => { + const corrector = service.getCorrector(); + + // Test with initial model + let result = await corrector.correct('AI uses GPT technology'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + + // Change model + service.setModel('gpt-4o-mini-transcribe'); + + // Dictionary corrections should still work + result = await corrector.correct('AI uses GPT technology'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + + // Change to other model + service.setModel('gpt-4o-transcribe'); + + // Dictionary corrections should still work + result = await corrector.correct('AI uses GPT technology'); + expect(result).toContain('artificial intelligence'); + expect(result).toContain('Generative Pre-trained Transformer'); + }); + }); +}); \ No newline at end of file