mirror of
https://github.com/mssoftjp/obsidian-voice-input.git
synced 2026-07-22 06:44:48 +00:00
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:
parent
538280ab7f
commit
249c9943c0
4 changed files with 409 additions and 4 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue