fix: address reviewbot required issues (#62)

* fix: address reviewbot feedback and update tests

Align request handling with Obsidian requestUrl and clean up UI text/lint directives while updating tests for the new mocks.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: ignore release_log.txt

Add release_log.txt to gitignore to keep local release logs out of version control.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: align requesturl handling with review feedback

centralize requesturl availability errors, bind provider defaults once, and stabilize timeout test.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address reviewbot required issues

Normalize UI sentence case and tighten error/string handling to satisfy Obsidian reviewbot checks.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(ui): restore brand casing in settings

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
asyouplz 2026-02-03 12:06:55 +09:00 committed by GitHub
parent e7e0e6b2cc
commit a87dc8f9d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 99 additions and 77 deletions

View file

@ -282,10 +282,22 @@ export class SettingsValidator {
typeof audio.sampleRate !== 'number' ||
!validRates.includes(audio.sampleRate)
) {
const sampleRateLabel =
typeof audio.sampleRate === 'number'
? audio.sampleRate
: JSON.stringify(audio.sampleRate) ?? String(audio.sampleRate);
const sampleRateLabel = (() => {
if (typeof audio.sampleRate === 'number') {
return String(audio.sampleRate);
}
if (typeof audio.sampleRate === 'string') {
return audio.sampleRate;
}
if (audio.sampleRate !== null && typeof audio.sampleRate === 'object') {
try {
return JSON.stringify(audio.sampleRate);
} catch {
return '[unserializable]';
}
}
return String(audio.sampleRate);
})();
errors.push({
field: 'audio.sampleRate',
message: `Invalid sample rate: ${sampleRateLabel}`,

View file

@ -774,7 +774,7 @@ export class DeepgramService {
json?: unknown;
text?: unknown;
headers?: Record<string, string>;
}): never {
}): Error {
const errorBody = isPlainRecord(response.json) ? response.json : undefined;
const rawText = typeof response.text === 'string' ? response.text : undefined;
const messageFromBody =
@ -792,7 +792,7 @@ export class DeepgramService {
switch (response.status) {
case 400:
throw new TranscriptionError(
return new TranscriptionError(
errorMessage || 'Invalid request',
'BAD_REQUEST',
'deepgram',
@ -800,9 +800,9 @@ export class DeepgramService {
400
);
case 401:
throw new ProviderAuthenticationError('deepgram');
return new ProviderAuthenticationError('deepgram');
case 402:
throw new TranscriptionError(
return new TranscriptionError(
'Insufficient credits',
'INSUFFICIENT_CREDITS',
'deepgram',
@ -811,7 +811,7 @@ export class DeepgramService {
);
case 429: {
const retryAfter = response.headers?.['retry-after'];
throw new ProviderRateLimitError(
return new ProviderRateLimitError(
'deepgram',
retryAfter ? parseInt(retryAfter) : undefined
);
@ -819,13 +819,13 @@ export class DeepgramService {
case 500:
case 502:
case 503:
throw new ProviderUnavailableError('deepgram');
return new ProviderUnavailableError('deepgram');
case 504: {
// Extract file size info if available
const sizeInfo = this.lastAudioSize
? ` (${Math.round(this.lastAudioSize / (1024 * 1024))}MB)`
: '';
throw new TranscriptionError(
return new TranscriptionError(
`Server timeout processing large audio file${sizeInfo}. This often happens with very large files (>50MB). Try: 1) Breaking the file into smaller chunks (recommended: <50MB per chunk), 2) Reducing audio quality/bitrate to 64-128 kbps, 3) Using the 'enhanced' model which may be faster, or 4) Converting to a more efficient format like MP3 or OGG.`,
'SERVER_TIMEOUT',
'deepgram',
@ -834,7 +834,7 @@ export class DeepgramService {
);
}
default:
throw new TranscriptionError(
return new TranscriptionError(
`API error: ${errorMessage}`,
'UNKNOWN_ERROR',
'deepgram',

View file

@ -341,7 +341,7 @@ export class DeepgramServiceRefactored {
status: number;
json?: unknown;
headers?: Record<string, string>;
}): never {
}): Error {
const errorBody = isPlainRecord(response.json) ? response.json : undefined;
const errorMessage =
(typeof errorBody?.message === 'string' ? errorBody.message : undefined) ??
@ -378,10 +378,10 @@ export class DeepgramServiceRefactored {
const errorFactory = errorMap[response.status];
if (errorFactory) {
throw errorFactory();
return errorFactory();
}
throw new TranscriptionError(
return new TranscriptionError(
`API error: ${errorMessage}`,
'UNKNOWN_ERROR',
'deepgram',

View file

@ -65,24 +65,22 @@ export function createSingleton<T>(factory: () => T): () => T {
/**
* Singleton
*/
export function SingletonDecorator<
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required by TS mixin constructor typing
T extends new (...args: any[]) => object
>(constructor: T): T {
let instance: InstanceType<T> | undefined;
export const SingletonDecorator: ClassDecorator = (constructor) => {
let instance: object | undefined;
return class extends constructor {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required by TS mixin constructor typing
constructor(...args: any[]) {
const proxy = new Proxy(constructor, {
construct(target, args, newTarget) {
if (instance) {
return instance;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- required by TS mixin constructor typing
super(...args);
instance = this as InstanceType<T>;
}
} as T;
}
const constructed = Reflect.construct(target, args, newTarget) as object;
instance = constructed;
return constructed;
},
});
return proxy;
};
/**
* Singleton

View file

@ -268,7 +268,7 @@ export class FormatOptionsModal extends Modal {
.setName('Language')
.setDesc('Programming language for syntax highlighting')
.addText((text) => {
text.setPlaceholder('JavaScript, Python, etc.')
text.setPlaceholder('JavaScript, python, etc.')
.setValue(this.options.codeLanguage || '')
.onChange((value) => {
this.options.codeLanguage = value;

View file

@ -261,6 +261,18 @@ export class MessageStore {
this.messages[key] = translations;
}
private static formatParamValue(value: unknown): string {
if (value === null) return 'null';
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch {
return '[unserializable]';
}
}
return String(value);
}
/**
*
*/
@ -272,7 +284,7 @@ export class MessageStore {
// 파라미터 치환
return message.replace(/\{(\w+)\}/g, (match: string, param: string) => {
const value = params[param];
return value !== undefined ? String(value) : match;
return value !== undefined ? this.formatParamValue(value) : match;
});
}

View file

@ -495,7 +495,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
new Notice(`${error}`);
}
} catch (error) {
new Notice('❌ Failed to validate API key.');
new Notice('❌ failed to validate API key.');
console.error(error);
} finally {
validateBtn.disabled = false;
@ -509,7 +509,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
if (api.provider === 'openai') {
new Setting(section)
.setName('Model')
.setDesc('Select the openai model')
.setDesc('Select the OpenAI model')
.addDropdown((dropdown) => {
dropdown.addOption('whisper-1', 'Whisper v1');
dropdown.setValue(api.model);
@ -586,7 +586,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
.addOption('webm', 'WebM (recommended)')
.addOption('mp3', 'MP3')
.addOption('m4a', 'M4A')
.addOption('wav', 'WAV (lossless)');
.addOption('wav', 'Wav (lossless)');
dropdown.setValue(audio.format);
dropdown.onChange(async (value) => {
@ -721,7 +721,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
});
new Setting(cacheSection)
.setName('Cache TTL (days)')
.setName('Cache ttl (days)')
.setDesc('Cache time-to-live in days')
.addSlider((slider) => {
slider
@ -919,7 +919,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
const links = section.createDiv({ cls: 'about-links' });
const githubLink = links.createEl('a', {
text: '📖 Documentation and guides',
text: '📖 documentation and guides',
href: 'https://github.com/yourusername/obsidian-speech-to-text',
});
githubLink.setAttribute('target', '_blank');
@ -927,7 +927,7 @@ export class EnhancedSettingsTab extends PluginSettingTab {
links.createEl('br');
const issueLink = links.createEl('a', {
text: '🐛 Report an issue',
text: '🐛 report an issue',
href: 'https://github.com/yourusername/obsidian-speech-to-text/issues',
});
issueLink.setAttribute('target', '_blank');

View file

@ -262,7 +262,7 @@ export class SettingsTab extends PluginSettingTab {
.addOption('performance_optimized', 'Performance optimized')
.addOption('quality_optimized', 'Quality optimized')
.addOption('round_robin', 'Round robin')
.addOption('ab_test', 'A/B testing')
.addOption('ab_test', 'A/b testing')
.setValue(
this.plugin.settings.selectionStrategy ||
SelectionStrategy.PERFORMANCE_OPTIMIZED
@ -368,7 +368,7 @@ export class SettingsTab extends PluginSettingTab {
.setName('OpenAI API key')
.setDesc('Enter your OpenAI API key for Whisper transcription')
.addText((text) => {
text.setPlaceholder('sk-...')
text.setPlaceholder('Sk-...')
.setValue(this.maskApiKey(this.plugin.settings.apiKey || ''))
.onChange(async (value) => {
if (value && !value.includes('*')) {

View file

@ -65,7 +65,7 @@ export class SimpleSettingsTab extends PluginSettingTab {
.setDesc('Enter your OpenAI API key for Whisper')
.addText((text) =>
text
.setPlaceholder('sk-...')
.setPlaceholder('Sk-...')
.setValue(this.plugin.settings.apiKey || '')
.onChange(async (value) => {
this.plugin.settings.apiKey = value;

View file

@ -70,7 +70,7 @@ export class ApiKeyValidator {
);
if (!hasWhisper) {
new Notice('⚠ API 키는 유효하지만 Whisper 모델 접근 권한이 없을 수 있습니다');
new Notice('⚠ API 키는 유효하지만 whisper 모델 접근 권한이 없을 수 있습니다');
}
return true;

View file

@ -38,7 +38,7 @@ export class AudioSettings {
// Whisper 모델 선택
new Setting(containerEl)
.setName('Whisper 모델')
.setDesc('사용할 Whisper 모델을 선택하세요')
.setDesc('사용할 whisper 모델을 선택하세요')
.addDropdown((dropdown) =>
dropdown
.addOption('whisper-1', 'Whisper-1 (기본)')
@ -61,8 +61,8 @@ export class AudioSettings {
.addOption('json', 'JSON (구조화된 데이터)')
.addOption('text', 'Text (일반 텍스트)')
.addOption('verbose_json', 'Verbose JSON (상세 정보)')
.addOption('srt', 'SRT (자막 형식)')
.addOption('vtt', 'VTT (WebVTT 형식)')
.addOption('srt', 'Srt (자막 형식)')
.addOption('vtt', 'Vtt (webvtt 형식)')
.setValue(
typeof this.plugin.settings['responseFormat'] === 'string'
? this.plugin.settings['responseFormat']
@ -111,7 +111,7 @@ export class AudioSettings {
// 파일 크기 제한
const fileSizeSetting = new Setting(containerEl)
.setName('최대 파일 크기')
.setDesc('업로드 가능한 최대 파일 크기 (MB)');
.setDesc('업로드 가능한 최대 파일 크기 (mb)');
const sizeValue = containerEl.createDiv({ cls: 'filesize-value' });
const currentSize = Math.round(this.plugin.settings.maxFileSize / (1024 * 1024));

View file

@ -614,7 +614,7 @@ export class DeepgramSettings {
});
noteEl.createEl('p', {
text: 'For best results with very large files (>100MB), consider:',
text: 'For best results with very large files (>100mb), consider:',
});
const secondaryList = noteEl.createEl('ul');
[

View file

@ -219,11 +219,11 @@ export class ProviderSettings {
.setDesc('How should the system choose between providers?')
.addDropdown((dropdown) => {
dropdown
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 Cost optimized')
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ Performance optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ Quality optimized')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 Round robin')
.addOption(SelectionStrategy.AB_TEST, '🧪 A/B testing')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 cost optimized')
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ performance optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ quality optimized')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 round robin')
.addOption(SelectionStrategy.AB_TEST, '🧪 a/b testing')
.setValue(
this.plugin.settings.selectionStrategy ||
SelectionStrategy.PERFORMANCE_OPTIMIZED
@ -349,7 +349,7 @@ export class ProviderSettings {
private renderMetrics(containerEl: HTMLElement): void {
const metricsEl = containerEl.createDiv({ cls: 'metrics-container' });
metricsEl.createEl('h4', { text: '📊 Performance metrics' });
metricsEl.createEl('h4', { text: '📊 performance metrics' });
// 각 Provider별 메트릭
this.renderProviderMetrics(metricsEl, 'whisper');
@ -405,7 +405,7 @@ export class ProviderSettings {
*/
private renderComparisonChart(containerEl: HTMLElement): void {
const chartEl = containerEl.createDiv({ cls: 'comparison-chart' });
chartEl.createEl('h5', { text: '📈 Provider comparison' });
chartEl.createEl('h5', { text: '📈 provider comparison' });
// 간단한 막대 차트 (실제로는 Chart.js 등 사용 권장)
const chartContent = chartEl.createDiv({ cls: 'chart-content' });
@ -432,10 +432,10 @@ export class ProviderSettings {
if (whisperConnected || deepgramConnected) {
statusEl.addClass('connected');
statusEl.setText('✅ Connected');
statusEl.setText('✅ connected');
} else {
statusEl.addClass('disconnected');
statusEl.setText('⚠️ No providers configured');
statusEl.setText('⚠️ no providers configured');
}
}

View file

@ -485,7 +485,7 @@ class ShortcutModal extends Modal {
this.isRecording = true;
this.recordedKeys.clear();
button.setButtonText('녹음 중... (ESC로 취소)');
button.setButtonText('녹음 중... (esc로 취소)');
button.buttonEl.addClass('is-recording');
// 키 이벤트 리스너

View file

@ -95,7 +95,7 @@ export class ProviderSettingsContainer {
// 타이틀
const titleEl = headerEl.createDiv({ cls: 'provider-title' });
new Setting(titleEl).setName('🎯 Transcription provider configuration').setHeading();
new Setting(titleEl).setName('🎯 transcription provider configuration').setHeading();
// 확장/축소 토글
const toggleBtn = headerEl.createEl('button', {
@ -233,10 +233,10 @@ export class ProviderSettingsContainer {
.setDesc('How should the system choose between providers?')
.addDropdown((dropdown) => {
dropdown
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ Performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 Cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ Quality first')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 Round robin')
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ quality first')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 round robin')
.setValue(
this.plugin.settings.selectionStrategy ||
SelectionStrategy.PERFORMANCE_OPTIMIZED
@ -579,10 +579,10 @@ export class ProviderSettingsContainer {
await new Promise((resolve) => setTimeout(resolve, 2000)); // 시뮬레이션
notice.hide();
new Notice('✅ Connection successful.', 3000);
new Notice('✅ connection successful.', 3000);
} catch (error) {
notice.hide();
new Notice('❌ Connection failed.', 3000);
new Notice('❌ connection failed.', 3000);
console.error('Connection test error:', error);
}
}
@ -841,10 +841,10 @@ class ProviderDetailsModal extends Modal {
await new Promise((resolve) => setTimeout(resolve, 2000));
notice.hide();
new Notice('✅ Connection test successful.', 3000);
new Notice('✅ connection test successful.', 3000);
} catch {
notice.hide();
new Notice('❌ Connection test failed.', 3000);
new Notice('❌ connection test failed.', 3000);
}
}
}
@ -856,7 +856,7 @@ class ProviderMetricsDisplay {
constructor(private plugin: SpeechToTextPlugin) {}
render(containerEl: HTMLElement): void {
new Setting(containerEl).setName('📊 Performance metrics').setHeading();
new Setting(containerEl).setName('📊 performance metrics').setHeading();
// TODO: 실제 메트릭 구현
const metricsEl = containerEl.createDiv({ cls: 'metrics-display' });

View file

@ -300,9 +300,9 @@ export class ProviderSettingsContainerRefactored extends BaseSettingsComponent {
this.createSetting(section, '선택 전략', 'Provider 선택 방법').addDropdown(
(dropdown) => {
dropdown
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ Performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 Cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ Quality first')
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ quality first')
.setValue(
this.plugin.settings.selectionStrategy ||
SelectionStrategy.PERFORMANCE_OPTIMIZED

View file

@ -819,7 +819,7 @@ export class APIKeyManager {
instructions.createEl('p', {
cls: 'warning',
text: '⚠️ Environment variables take precedence over saved keys',
text: '⚠️ environment variables take precedence over saved keys',
});
modal.open();

View file

@ -63,7 +63,7 @@ export class AdvancedSettingsPanel {
const headerEl = containerEl.createDiv({ cls: 'advanced-panel-header' });
headerEl.createEl('h4', {
text: '⚙️ Advanced configuration',
text: '⚙️ advanced configuration',
cls: 'advanced-title',
});
@ -96,11 +96,11 @@ export class AdvancedSettingsPanel {
.setDesc('How the system selects providers when in auto mode')
.addDropdown((dropdown) => {
dropdown
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ Performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 Cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ Quality first')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 Round robin')
.addOption(SelectionStrategy.AB_TEST, '🧪 A/B testing')
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ performance first')
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 cost optimized')
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ quality first')
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 round robin')
.addOption(SelectionStrategy.AB_TEST, '🧪 a/b testing')
.setValue(
this.plugin.settings.selectionStrategy ||
SelectionStrategy.PERFORMANCE_OPTIMIZED
@ -880,7 +880,7 @@ export class AdvancedSettingsPanel {
*/
private showABTestResults(): void {
// TODO: Implement A/B test results modal
new Notice('A/B test results coming soon.');
new Notice('A/b test results coming soon.');
}
/**