mirror of
https://github.com/asyouplz/SpeechNote.git
synced 2026-07-22 16:30:31 +00:00
- Set up project structure and architecture - Implement core transcription services with OpenAI Whisper API - Add comprehensive UI components and settings - Create documentation in Korean and English - Establish testing framework and examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
20 KiB
20 KiB
컴포넌트 가이드 (Component Guide)
목차
개요
이 문서는 Speech-to-Text 플러그인의 주요 컴포넌트들의 구조, 사용법, 통합 방법을 설명합니다.
컴포넌트 아키텍처
graph TD
A[Main Plugin] --> B[Application Layer]
A --> C[Infrastructure Layer]
A --> D[UI Layer]
B --> B1[EditorService]
B --> B2[TextInsertionHandler]
B --> B3[EventManager]
B --> B4[StateManager]
C --> C1[WhisperService]
C --> C2[SettingsManager]
C --> C3[FileUploadManager]
C --> C4[Logger]
D --> D1[FilePickerModal]
D --> D2[DragDropZone]
D --> D3[ProgressIndicator]
D --> D4[RecentFiles]
UI 컴포넌트
DragDropZone
파일 드래그 앤 드롭 기능을 제공하는 컴포넌트입니다.
기본 사용법
import { DragDropZone } from './ui/components/DragDropZone';
const dropZone = new DragDropZone(container, {
onFileDrop: async (files) => {
for (const file of files) {
await processFile(file);
}
},
acceptedFormats: ['.m4a', '.mp3', '.wav'],
maxFileSize: 25 * 1024 * 1024
});
// 활성화/비활성화
dropZone.setEnabled(true);
// 정리
dropZone.destroy();
주요 기능
- 파일 검증: 형식과 크기 자동 검증
- 시각적 피드백: 드래그 오버 시 하이라이트
- 다중 파일 지원: 여러 파일 동시 처리
- 에러 처리: 잘못된 파일 형식 알림
FilePickerModal
파일 선택 모달 다이얼로그 컴포넌트입니다.
기본 사용법
import { FilePickerModal } from './ui/modals/FilePickerModal';
class AudioFilePickerModal extends FilePickerModal {
constructor(app: App, onSelect: (file: TFile) => void) {
super(app);
this.onSelect = onSelect;
}
protected getItems(): TFile[] {
return this.app.vault.getFiles()
.filter(f => this.isAudioFile(f));
}
protected onChooseItem(file: TFile): void {
this.onSelect(file);
}
}
// 사용
const modal = new AudioFilePickerModal(app, async (file) => {
await transcriptionService.transcribe(file);
});
modal.open();
주요 기능
- 검색 기능: 실시간 파일 검색
- 최근 파일: 최근 사용한 파일 표시
- 파일 미리보기: 파일 정보 표시
- 키보드 단축키: Enter/Esc 지원
ProgressIndicator
진행 상황을 표시하는 컴포넌트입니다.
기본 사용법
import { ProgressIndicator } from './ui/components/ProgressIndicator';
const progress = new ProgressIndicator();
// 진행 상황 업데이트
progress.update({
percentage: 50,
message: '파일 처리 중...',
status: 'processing'
});
// 완료
progress.complete('변환 완료!');
// 에러
progress.error('변환 실패');
// 숨기기
progress.hide();
상태 타입
type ProgressStatus =
| 'idle' // 대기
| 'preparing' // 준비 중
| 'uploading' // 업로드 중
| 'processing' // 처리 중
| 'completed' // 완료
| 'error'; // 에러
RecentFiles
최근 사용한 파일 목록을 관리하는 컴포넌트입니다.
기본 사용법
import { RecentFiles } from './ui/components/RecentFiles';
const recentFiles = new RecentFiles(maxItems = 10);
// 파일 추가
recentFiles.add(file);
// 목록 가져오기
const files = recentFiles.getAll();
// 특정 파일 제거
recentFiles.remove(file.path);
// 전체 초기화
recentFiles.clear();
서비스 컴포넌트
WhisperService
OpenAI Whisper API와 통신하는 핵심 서비스입니다.
초기화 및 설정
import { WhisperService } from './infrastructure/api/WhisperService';
import { Logger } from './infrastructure/logging/Logger';
const logger = new Logger('WhisperService');
const whisperService = new WhisperService(apiKey, logger);
음성 변환
// 기본 변환
const response = await whisperService.transcribe(audioBuffer);
// 옵션과 함께 변환
const response = await whisperService.transcribe(audioBuffer, {
language: 'ko',
prompt: '이전 대화 컨텍스트...',
temperature: 0.2,
responseFormat: 'verbose_json'
});
// 세그먼트 정보 포함
if (response.segments) {
response.segments.forEach(segment => {
console.log(`[${segment.start}-${segment.end}] ${segment.text}`);
});
}
에러 처리
try {
const response = await whisperService.transcribe(audioBuffer);
} catch (error) {
if (error instanceof AuthenticationError) {
// API 키 문제
await settingsManager.setApiKey('');
} else if (error instanceof RateLimitError) {
// 재시도 로직
setTimeout(() => retry(), error.retryAfter * 1000);
} else if (error instanceof FileTooLargeError) {
// 파일 압축 필요
const compressed = await compressAudio(audioBuffer);
await whisperService.transcribe(compressed);
}
}
SettingsManager
플러그인 설정을 관리하는 서비스입니다.
설정 로드 및 저장
import { SettingsManager } from './infrastructure/storage/SettingsManager';
const settingsManager = new SettingsManager(plugin, logger);
// 설정 로드
const settings = await settingsManager.load();
// 개별 설정 값 가져오기
const language = settingsManager.get('language');
const autoInsert = settingsManager.get('autoInsert');
// 설정 업데이트
await settingsManager.set('language', 'ko');
await settingsManager.set('temperature', 0.3);
// 전체 설정 저장
await settingsManager.save({
...settings,
language: 'en'
});
API 키 관리
// API 키 설정 (자동 암호화)
const success = await settingsManager.setApiKey('sk-...');
// API 키 가져오기 (복호화됨)
const apiKey = settingsManager.getApiKey();
// 마스킹된 API 키 표시
const masked = settingsManager.getMaskedApiKey();
console.log(masked); // "sk-XXXXX...XXXX"
FileUploadManager
대용량 파일 처리 및 업로드를 관리합니다.
파일 처리
import { FileUploadManager } from './infrastructure/api/FileUploadManager';
const uploadManager = new FileUploadManager(vault, logger);
// 진행 상황 콜백과 함께 파일 처리
const processed = await uploadManager.prepareAudioFile(file, (progress) => {
console.log(`${progress.percentage}% - ${progress.message}`);
progressIndicator.update(progress);
});
console.log(`원본: ${processed.originalSize}, 처리 후: ${processed.processedSize}`);
청크 업로드
// 대용량 파일을 청크로 나누어 업로드
for await (const chunk of uploadManager.uploadInChunks(buffer)) {
await sendChunkToServer(chunk);
}
// 취소 기능
uploadManager.cancel();
EditorService
Obsidian 에디터와의 통합을 담당합니다.
텍스트 삽입
import { EditorService } from './application/EditorService';
const editorService = new EditorService(app, eventManager, logger);
// 커서 위치에 삽입
await editorService.insertAtCursor("변환된 텍스트");
// 선택 영역 대체
await editorService.replaceSelection("새로운 텍스트");
// 문서 끝에 추가
await editorService.appendToDocument("\n\n## 추가 내용");
// 특정 위치에 삽입
const position = { line: 10, ch: 0 };
await editorService.insertAtPosition("삽입할 텍스트", position);
Undo/Redo 지원
// 작업 실행 (자동으로 히스토리에 기록)
await editorService.insertAtCursor("텍스트", true);
// Undo
await editorService.undo();
// Redo
await editorService.redo();
// 히스토리 초기화
editorService.clearHistory();
TextInsertionHandler
텍스트 포맷팅과 삽입을 처리합니다.
포맷팅 옵션
import { TextInsertionHandler } from './application/TextInsertionHandler';
const insertionHandler = new TextInsertionHandler(
editorService,
eventManager,
logger
);
// 인용구 형식으로 삽입
await insertionHandler.insertText("변환된 텍스트", {
mode: 'cursor',
format: 'quote',
quoteAuthor: 'Speaker Name'
});
// 코드 블록으로 삽입
await insertionHandler.insertText("코드 내용", {
mode: 'cursor',
format: 'code',
codeLanguage: 'javascript'
});
// 타임스탬프와 함께 삽입
await insertionHandler.insertText("텍스트", {
mode: 'cursor',
format: 'plain',
addTimestamp: true,
timestampFormat: 'YYYY-MM-DD HH:mm:ss'
});
템플릿 사용
// 템플릿 적용
await insertionHandler.insertText("내용", {
mode: 'cursor',
format: 'plain',
template: '## {{date}} 회의록\n\n{{content}}\n\n---\n작성: {{time}}'
});
EventManager
애플리케이션 전체의 이벤트를 관리합니다.
이벤트 구독
import { EventManager } from './application/EventManager';
const eventManager = new EventManager(logger);
// 이벤트 리스너 등록
const unsubscribe = eventManager.on('transcription:complete', (data) => {
console.log(`변환 완료: ${data.text.substring(0, 50)}...`);
if (data.autoInsert) {
insertionHandler.insertText(data.text, insertOptions);
}
});
// 일회성 리스너
eventManager.once('file:selected', (data) => {
console.log(`파일 선택됨: ${data.file.path}`);
});
// 구독 해제
unsubscribe();
고급 기능
// 이벤트 디바운싱 (연속 호출 방지)
eventManager.debounce('settings:changed', 500, (data) => {
saveSettings(data);
});
// 이벤트 쓰로틀링 (호출 빈도 제한)
eventManager.throttle('transcription:progress', 100, (data) => {
updateProgressBar(data.progress);
});
// 이벤트 체인
eventManager.chain(
'transcription:complete',
'editor:text-inserted',
(data) => ({ text: data.text, position: 'cursor' })
);
// 이벤트 필터링
eventManager.filter(
'file:validated',
(data) => data.valid === true,
(data) => processValidFile(data.file)
);
패턴 구현
Singleton Pattern
전역적으로 단일 인스턴스를 보장합니다.
import { SingletonDecorator } from './patterns/Singleton';
@SingletonDecorator
class GlobalStateManager {
private state: AppState;
getState(): AppState {
return this.state;
}
}
// 어디서든 동일한 인스턴스
const manager1 = new GlobalStateManager();
const manager2 = new GlobalStateManager();
console.log(manager1 === manager2); // true
Observer Pattern
이벤트 기반 통신을 구현합니다.
import { EventEmitter } from './patterns/Observer';
interface CustomEvents {
'data:updated': { id: string; value: any };
'error:occurred': { error: Error };
}
class DataService extends EventEmitter<CustomEvents> {
updateData(id: string, value: any) {
// 데이터 업데이트
this.emit('data:updated', { id, value });
}
}
Factory Pattern
객체 생성을 추상화합니다.
import { ServiceFactory } from './patterns/Factory';
const factory = new ServiceFactory();
// 서비스 등록
factory.register('whisper', () => new WhisperService(apiKey, logger));
factory.register('settings', () => new SettingsManager(plugin));
// 서비스 생성
const whisperService = factory.create('whisper');
const settingsManager = factory.create('settings');
통합 예제
완전한 음성 변환 플로우
class SpeechToTextWorkflow {
constructor(
private whisperService: WhisperService,
private uploadManager: FileUploadManager,
private editorService: EditorService,
private insertionHandler: TextInsertionHandler,
private eventManager: EventManager,
private logger: Logger
) {}
async processAudioFile(file: TFile): Promise<void> {
try {
// 1. 시작 이벤트 발생
this.eventManager.emit('transcription:start', {
fileName: file.name,
fileSize: file.stat.size
});
// 2. 파일 준비 및 검증
const processed = await this.uploadManager.prepareAudioFile(
file,
(progress) => {
this.eventManager.emit('transcription:progress', {
progress: progress.percentage * 0.3, // 0-30%
message: progress.message
});
}
);
// 3. API 호출
this.eventManager.emit('transcription:progress', {
progress: 30,
message: 'Transcribing audio...'
});
const response = await this.whisperService.transcribe(
processed.buffer,
{
language: this.settings.language,
responseFormat: 'verbose_json'
}
);
// 4. 텍스트 포맷팅 및 삽입
this.eventManager.emit('transcription:progress', {
progress: 90,
message: 'Formatting text...'
});
await this.insertionHandler.insertText(response.text, {
mode: this.settings.insertPosition,
format: 'plain',
addTimestamp: this.settings.timestampFormat !== 'none',
language: response.language
});
// 5. 완료 이벤트
this.eventManager.emit('transcription:complete', {
text: response.text,
duration: processed.metadata.duration || 0
});
// 6. 성공 알림
new Notice(`Transcription complete! (${response.language})`);
} catch (error) {
// 에러 처리
this.eventManager.emit('transcription:error', {
error: error as Error,
fileName: file.name
});
this.logger.error('Transcription failed', error as Error);
new Notice(`Failed: ${(error as Error).message}`);
}
}
}
설정 UI 통합
class SettingsTab extends PluginSettingTab {
constructor(
app: App,
plugin: Plugin,
private settingsManager: SettingsManager,
private whisperService: WhisperService
) {
super(app, plugin);
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// API 키 설정
new Setting(containerEl)
.setName('OpenAI API Key')
.setDesc('Your OpenAI API key for Whisper')
.addText(text => text
.setPlaceholder('sk-...')
.setValue(this.settingsManager.getMaskedApiKey())
.onChange(async (value) => {
if (value && !value.includes('*')) {
const isValid = await this.whisperService.validateApiKey(value);
if (isValid) {
await this.settingsManager.setApiKey(value);
new Notice('API key validated and saved');
} else {
new Notice('Invalid API key');
}
}
})
);
// 언어 설정
new Setting(containerEl)
.setName('Language')
.setDesc('Transcription language')
.addDropdown(dropdown => dropdown
.addOptions({
'auto': 'Auto-detect',
'ko': 'Korean',
'en': 'English',
'ja': 'Japanese',
'zh': 'Chinese'
})
.setValue(this.settingsManager.get('language'))
.onChange(async (value) => {
await this.settingsManager.set('language', value);
})
);
}
}
베스트 프랙티스
1. 에러 처리
항상 try-catch로 에러를 처리하고 사용자에게 적절한 피드백을 제공하세요.
try {
await riskyOperation();
} catch (error) {
// 로깅
logger.error('Operation failed', error);
// 사용자 알림
new Notice(errorHandler.getUserMessage(error));
// 이벤트 발생
eventManager.emit('error:occurred', { error });
// 복구 가능한 경우 재시도
if (errorHandler.isRetryable(error)) {
await retry(riskyOperation);
}
}
2. 리소스 정리
컴포넌트 사용 후 항상 정리하세요.
class MyComponent {
private subscriptions: Unsubscribe[] = [];
initialize() {
// 이벤트 구독
this.subscriptions.push(
eventManager.on('event', this.handleEvent)
);
}
destroy() {
// 모든 구독 해제
this.subscriptions.forEach(unsub => unsub());
// 리소스 정리
this.uploadManager?.cleanup();
this.whisperService?.cancel();
}
}
3. 비동기 작업 관리
class AsyncOperationManager {
private operations = new Set<Promise<any>>();
async execute<T>(operation: () => Promise<T>): Promise<T> {
const promise = operation();
this.operations.add(promise);
try {
const result = await promise;
return result;
} finally {
this.operations.delete(promise);
}
}
async cancelAll() {
await Promise.allSettled(this.operations);
this.operations.clear();
}
}
4. 타입 안전성
TypeScript의 타입 시스템을 최대한 활용하세요.
// 타입 가드 사용
function isWhisperResponse(obj: any): obj is WhisperResponse {
return obj && typeof obj.text === 'string';
}
// 제네릭 활용
function createService<T extends IService>(
ServiceClass: new (...args: any[]) => T,
...args: any[]
): T {
return new ServiceClass(...args);
}
// 유틸리티 타입
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
5. 성능 최적화
// 디바운싱
const debouncedSave = debounce(saveSettings, 500);
// 메모이제이션
const memoizedExpensiveOperation = memoize(expensiveOperation);
// 레이지 로딩
const LazyService = lazy(() => import('./HeavyService'));
컴포넌트 생명주기
초기화 순서
- Logger 초기화: 로깅 시스템 설정
- EventManager 생성: 이벤트 시스템 준비
- Settings 로드: 설정 불러오기
- Services 초기화: 각 서비스 인스턴스 생성
- UI 컴포넌트 생성: UI 요소 초기화
- 이벤트 리스너 등록: 이벤트 구독 설정
종료 순서
- 진행 중인 작업 취소: 모든 비동기 작업 중단
- 이벤트 리스너 해제: 구독 정리
- UI 컴포넌트 정리: DOM 요소 제거
- 서비스 정리: 리소스 해제
- 설정 저장: 마지막 상태 저장
- Logger 종료: 로그 플러시
디버깅 도구
개발 모드 활성화
// 개발 환경에서만 디버그 모드 활성화
if (process.env.NODE_ENV === 'development') {
eventManager.setDebugMode(true);
logger.setLevel(LogLevel.DEBUG);
// 전역 디버그 객체
window.speechToTextDebug = {
services: {
whisper: whisperService,
settings: settingsManager,
editor: editorService
},
eventManager,
logger
};
}
이벤트 모니터링
// 모든 이벤트 로깅
const events = [
'transcription:start',
'transcription:progress',
'transcription:complete',
'transcription:error'
];
events.forEach(event => {
eventManager.on(event as any, (data) => {
console.log(`[Event] ${event}:`, data);
});
});
// 통계 확인
console.log('Event Stats:', eventManager.getStats());
console.log('Event History:', eventManager.getHistory());
최종 업데이트: 2025-08-22
버전: 1.0.0