feat(transcription): enable multilingual dictionary correction for all languages

- Fix settings synchronization between TranscriptionService and DictionaryCorrector
- Add comprehensive test coverage for multilingual dictionary correction
- Update README documentation to reflect multilingual support
- Ensure enableTranscriptionCorrection setting applies consistently across all components

Co-authored-by: mssoftjp <115339791+mssoftjp@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-08-12 18:59:57 +00:00
parent 538280ab7f
commit 249c9943c0
4 changed files with 409 additions and 4 deletions

View file

@ -9,7 +9,7 @@ High-accuracy voice input for Obsidian. Uses OpenAI GPT-4o Audio Transcriptions
- Model selection: GPT4o Transcribe or GPT4o mini Transcribe
- Language separation: independent UI language and voice recognition language settings
- Auto language detection: automatic voice recognition language based on Obsidian locale
- AI postprocessing: optional dictionary-based cleanup (Japanese only)
- AI postprocessing: optional dictionary-based cleanup (applied to all languages when enabled)
- Quick controls in view: copy/clear/insert at cursor/append to end
- Autosave 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 plugins 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 Postprocessing: enable dictionarybased cleanup (applied to Japanese only)
- AI Postprocessing: enable dictionarybased cleanup (applied to all languages when enabled)
- Maximum Recording Duration: slider (default 5 min)
- Plugin Language: English/Japanese (controls UI display only, autodetected from Obsidian, adjustable)

View file

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

View file

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

View file

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