hgzero/design/구현방안-STT.md
Minseo-Jo afbfc7f947 STT 구현 방안 문서 작성
- 음성인식(STT) 기술 개요 및 한국어 처리 특징 정리
- OpenAI Whisper API와 AWS Transcribe 비교 분석
- 실시간/배치 처리 방식별 아키텍처 설계
- WebSocket 기반 실시간 STT 처리 플로우 정의
- 성능 최적화 및 정확도 개선 방안 제시
- 비용 분석 및 모니터링 전략 수립

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 13:52:30 +09:00

1081 lines
35 KiB
Markdown

# STT (Speech-to-Text) 구현방안
## 📋 문서 정보
- **작성일**: 2025-10-21
- **최종 수정일**: 2025-10-21
- **작성자**: 회의록 서비스 개발팀
- **버전**: 2.0
- **검토자**: 박서연(AI), 이준호(Backend), 이동욱(Backend), 최유진(Frontend), 홍길동(Architect), 정도현(QA)
- **STT 엔진**: Azure Speech Services (실시간 스트리밍 + 화자 식별)
---
## 1. 개요
### 1.1 목적
회의 참석자의 발언을 실시간으로 음성 인식하여 텍스트로 변환하고, AI 기반 회의록 자동 작성의 기반 데이터를 제공합니다.
### 1.2 핵심 요구사항
- **실시간성**: 발언 후 1초 이내 화면 표시 (Azure 실시간 스트리밍)
- **정확도**: STT confidence score 90% 이상
- **화자 식별**: 참석자별 발언 자동 구분 (Azure Speaker Diarization)
- **안정성**: 네트워크 장애 시에도 녹음 데이터 보존
### 1.3 Azure Speech Services 선정 이유
-**실시간 스트리밍**: 1초 이내 지연 시간으로 요구사항 충족
-**화자 식별 기본 제공**: Speaker Diarization 내장 (별도 구현 불필요)
-**한국어 최적화**: Microsoft의 한국어 특화 모델로 높은 정확도
-**엔터프라이즈 안정성**: 99.9% SLA 보장
-**Azure 생태계 통합**: 향후 Azure 기반 인프라 확장 용이
### 1.4 차별화 전략
STT 자체는 기본 기능(Hygiene Factor)이나, 다음 차별화 요소와 연계됩니다:
- 맥락 기반 용어 설명 (RAG)
- AI 회의록 자동 작성
- Todo 자동 추출
---
## 2. 아키텍처 설계
### 2.1 전체 구조
```
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │─────▶│ STT Gateway │─────▶│ Azure Speech │
│ (Browser) │ │ Service │ │ Services │
│ │ │ │ │ (실시간 스트리밍)│
└─────────────┘ └──────────────┘ └─────────────────┘
│ │ │
│ │ │
│ │ ┌───────▼───────┐
│ │ │ Speaker │
│ │ │ Diarization │
│ │ │ (화자 식별) │
│ │ └───────────────┘
│ │ │
▼ │ │
┌─────────────┐ ┌──────▼──────┐ ┌───────▼─────┐
│ WebSocket │◀─────│ RabbitMQ │◀─────│ Claude API │
│ Server │ │ Queue │ │ (후처리) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└─────────────▶│ Redis │
│ Cache │
└─────────────┘
```
### 2.2 계층별 역할
#### **Client Layer (Frontend)**
- **MediaRecorder API**: 브라우저에서 실시간 음성 캡처
- **WebSocket Client**: 실시간 텍스트 수신 및 화면 동기화
- **로컬 저장**: 네트워크 장애 시 음성 데이터 임시 저장
#### **STT Gateway Service**
- **오디오 스트림 수신**: 클라이언트로부터 실시간 음성 스트림 수신
- **Azure Speech 연동**: Azure Speech Services 실시간 스트리밍 API 호출
- **화자 식별 처리**: Azure Speaker Diarization 결과 수신 및 참석자 매칭
- **이벤트 발행**: RabbitMQ에 `TextTranscribed` 이벤트 발행
#### **Azure Speech Services**
- **실시간 스트리밍 STT**: 음성을 실시간으로 텍스트 변환 (< 1초 지연)
- **Speaker Diarization**: 화자별 발언 자동 구분
- **언어 모델**: 한국어 특화 최적화 모델
- **신뢰도 점수**: 발언에 대한 confidence score 제공
#### **Message Queue (RabbitMQ)**
- **비동기 처리**: STT 결과를 비동기로 후속 서비스에 전달
- **이벤트 라우팅**: `TextTranscribed` AI Service, Meeting Service
- **재시도 로직**: 실패 자동 재처리 (최대 3회)
#### **AI Service (Claude API)**
- **텍스트 후처리**: 구어체 문어체 변환, 문법 교정
- **회의록 구조화**: 템플릿에 맞춰 내용 정리
- **Todo 추출**: 액션 아이템 자동 식별
#### **Cache Layer (Redis)**
- **실시간 발언 캐싱**: `meeting:{meeting_id}:live_text`
- **섹션별 내용 캐싱**: `meeting:{meeting_id}:sections:{section_id}`
- **화자 정보 캐싱**: `meeting:{meeting_id}:speakers`
#### **WebSocket Server**
- **실시간 동기화**: 모든 참석자에게 텍스트 변환 결과 즉시 전송
- **Delta 전송**: 변경된 부분만 전송하여 대역폭 최적화
---
## 3. 데이터 구조 설계
### 3.1 Azure Speech 스트리밍 연결 설정
```json
{
"session_id": "SESSION_001",
"meeting_id": "MTG_001",
"config": {
"language": "ko-KR",
"sample_rate": 16000,
"format": "audio/wav",
"enable_diarization": true,
"max_speakers": 10,
"profanity_filter": "masked",
"enable_dictation": true
},
"participants": [
{
"user_id": "USR_001",
"name": "김철수",
"voice_signature": null
},
{
"user_id": "USR_002",
"name": "이영희",
"voice_signature": null
}
]
}
```
### 3.2 실시간 오디오 스트림 전송 (WebSocket)
```json
{
"type": "audio_chunk",
"session_id": "SESSION_001",
"audio_data": "base64_encoded_audio",
"timestamp": "2025-10-21T14:30:15.000Z",
"sequence": 42
}
```
### 3.3 Azure Speech 실시간 응답 (WebSocket)
```json
{
"type": "recognition_result",
"session_id": "SESSION_001",
"result_id": "RESULT_001",
"recognition_status": "Success",
"duration": 4500000000,
"offset": 0,
"text": "회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의입니다.",
"confidence": 0.95,
"speaker_id": "Speaker_1",
"lexical": "회의를 시작하겠습니다 오늘은 프로젝트 킥오프 회의입니다",
"itn": "회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의입니다.",
"display": "회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의입니다.",
"words": [
{
"word": "회의를",
"offset": 0,
"duration": 400000000,
"confidence": 0.96
},
{
"word": "시작하겠습니다",
"offset": 400000000,
"duration": 1100000000,
"confidence": 0.94
}
],
"is_final": true,
"timestamp": "2025-10-21T14:30:16.000Z"
}
```
### 3.4 화자 매칭 결과 (STT Gateway 내부 처리)
```json
{
"result_id": "RESULT_001",
"azure_speaker_id": "Speaker_1",
"matched_user": {
"user_id": "USR_001",
"name": "김철수",
"confidence": 0.88
},
"matching_method": "voice_pattern",
"timestamp": "2025-10-21T14:30:16.000Z"
}
```
### 3.5 Claude API 호출 구조
#### **요청 (STT Gateway → Claude API)**
```json
{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 2048,
"messages": [
{
"role": "user",
"content": "다음은 회의 발언 내용입니다. 회의록 형식에 맞춰 정리해주세요.\n\n발언: \"회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의입니다.\"\n화자: 김철수\n시간: 2025-10-21 14:30:15\n\n템플릿 섹션: 안건, 논의 내용, 결정 사항, Todo"
}
],
"temperature": 0.3,
"system": "당신은 회의록 작성 전문가입니다. 발언 내용을 구조화하여 명확하고 간결하게 정리합니다."
}
```
#### **응답 (Claude API → AI Service)**
```json
{
"id": "msg_01XYZ...",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "## 안건\n- 프로젝트 킥오프 회의 진행\n\n## 논의 내용\n- (발언 내용을 기반으로 자동 작성됩니다)\n\n## 결정 사항\n- (아직 결정된 사항 없음)\n\n## Todo\n- (아직 할당된 작업 없음)"
}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 245,
"output_tokens": 128
}
}
```
### 3.4 RabbitMQ 이벤트 구조
```json
{
"event_type": "TextTranscribed",
"event_id": "EVT_001",
"timestamp": "2025-10-21T14:30:18.000Z",
"correlation_id": "CORR_001",
"payload": {
"meeting_id": "MTG_001",
"speaker": {
"id": "USR_001",
"name": "김철수"
},
"transcription": {
"text": "회의를 시작하겠습니다. 오늘은 프로젝트 킥오프 회의입니다.",
"confidence": 0.95,
"segments": [...]
},
"timestamp": "2025-10-21T14:30:15.000Z"
},
"metadata": {
"source": "stt-gateway-service",
"version": "1.0"
}
}
```
### 3.6 Redis 캐시 구조
```javascript
// 1. 실시간 발언 (TTL: 10분)
Key: "meeting:MTG_001:live_text"
Value: {
"speaker": "김철수",
"text": "회의를 시작하겠습니다...",
"timestamp": "2025-10-21T14:30:15.000Z",
"is_final": true
}
// 2. 섹션별 내용 (TTL: 회의 종료 후 1시간)
Key: "meeting:MTG_001:sections:agenda"
Value: {
"section_id": "agenda",
"section_name": "안건",
"content": "프로젝트 킥오프 회의 진행\n- 프로젝트 목표 및 범위 확정\n- 역할 분담 및 일정 계획",
"verified": false,
"last_updated": "2025-10-21T14:32:00.000Z"
}
// 3. 화자 정보 (TTL: 회의 종료 후 1시간)
Key: "meeting:MTG_001:speakers"
Value: [
{
"id": "USR_001",
"name": "김철수",
"role": "주관자",
"speech_count": 15,
"speech_duration_ms": 180000
},
{
"id": "USR_002",
"name": "이영희",
"role": "참석자",
"speech_count": 12,
"speech_duration_ms": 150000
}
]
// 4. 회의 메타데이터 (TTL: 회의 종료 후 24시간)
Key: "meeting:MTG_001:metadata"
Value: {
"meeting_id": "MTG_001",
"title": "프로젝트 킥오프 회의",
"status": "in_progress",
"start_time": "2025-10-21T14:00:00.000Z",
"participants": ["USR_001", "USR_002", "USR_003"],
"total_speech_count": 42,
"last_activity": "2025-10-21T14:32:00.000Z"
}
```
### 3.7 WebSocket 실시간 동기화 메시지
```json
{
"type": "transcription_update",
"message_id": "WS_MSG_001",
"timestamp": "2025-10-21T14:30:18.000Z",
"data": {
"meeting_id": "MTG_001",
"speaker": {
"id": "USR_001",
"name": "김철수"
},
"transcription": {
"text": "회의를 시작하겠습니다.",
"is_final": true,
"confidence": 0.95
},
"target_section": "agenda",
"action": "append"
}
}
```
---
## 4. 처리 흐름 (Sequence)
### 4.1 실시간 스트리밍 흐름
```
Client STT Gateway Azure Speech RabbitMQ AI Service WebSocket Server
│ │ │ │ │ │
│─1.WebSocket 연결─▶│ │ │ │ │
│ │─2.Speech 세션─▶│ │ │ │
│ │ 시작 │ │ │ │
│ │◀─3.세션 준비───│ │ │ │
│ │ │ │ │ │
│─4.실시간 음성──▶│ │ │ │ │
│ 스트림 전송 │─5.오디오 전송─▶│ │ │ │
│ │ │ │ │ │
│ │◀─6.실시간 텍스트│ │ │ │
│ │ (화자 식별) │ │ │ │
│ │ │ │ │ │
│ │──────7.이벤트 발행─────────▶│ │ │
│ │ │ │──8.구독──▶│ │
│ │ │ │ │──9.Claude──▶│
│ │ │ │ │ 후처리 │
│◀────────────────────────────────────────────────────────10.실시간 동기화────│
```
**단계별 설명:**
1. **Client**: WebSocket으로 STT Gateway 연결
2. **STT Gateway**: Azure Speech Services 스트리밍 세션 시작
3. **Azure Speech**: 세션 준비 완료 응답
4. **Client**: MediaRecorder로 실시간 음성 스트림 전송
5. **STT Gateway**: Azure Speech로 오디오 스트림 전달
6. **Azure Speech**: 실시간 텍스트 변환 + 화자 식별 (< 1초 지연)
7. **STT Gateway**: RabbitMQ에 `TextTranscribed` 이벤트 발행
8. **AI Service**: RabbitMQ 구독하여 이벤트 수신
9. **AI Service**: Claude API로 텍스트 후처리 (구조화, 요약)
10. **WebSocket Server**: 모든 참석자에게 실시간 동기화
### 4.2 화자 식별 흐름
```
Azure Speech STT Gateway Redis Cache Participants DB
│ │ │ │
│─1.Speaker_1───▶│ │ │
│ 인식 결과 │ │ │
│ │─2.Speaker_1──▶│ │
│ │ 매핑 조회 │ │
│ │◀─3.매핑 없음──│ │
│ │ │ │
│ │────────4.참석자 목록 조회────────▶│
│ │◀───────5.참석자 목록──────────────│
│ │ │ │
│ │─6.음성 패턴───│ │
│ │ 기반 매칭 │ │
│ │ │ │
│ │─7.Speaker_1 =─▶│ │
│ │ USR_001 저장 │ │
```
**화자 매칭 전략:**
1. **첫 발언**: Azure가 제공한 Speaker_1, Speaker_2 등을 참석자 목록과 매칭
2. **음성 패턴 분석**: 발언 순서, 발언 빈도, 음성 특징 기반 추정
3. **Redis 캐싱**: 매칭 결과를 캐싱하여 이후 발언에 재사용
4. **수동 보정**: 사용자가 화자를 수동으로 지정 가능
---
## 5. 구현 상세
### 5.1 Frontend (React)
#### **음성 캡처 및 WebSocket 스트리밍**
```javascript
// Azure Speech 실시간 스트리밍
class AzureSpeechRecorder {
constructor(meetingId, speakerId) {
this.meetingId = meetingId;
this.speakerId = speakerId;
this.ws = null;
this.mediaRecorder = null;
this.audioContext = null;
}
async start() {
// WebSocket 연결
this.ws = new WebSocket(`ws://localhost:3001/api/stt/stream`);
this.ws.onopen = () => {
// 세션 시작 요청
this.ws.send(JSON.stringify({
type: 'session_start',
session_id: `SESSION_${Date.now()}`,
meeting_id: this.meetingId,
config: {
language: 'ko-KR',
sample_rate: 16000,
format: 'audio/wav',
enable_diarization: true,
max_speakers: 10
}
}));
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'session_ready') {
this.startRecording();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
async startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000, // Azure 권장
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// AudioContext로 PCM 변환
this.audioContext = new AudioContext({ sampleRate: 16000 });
const source = this.audioContext.createMediaStreamSource(stream);
const processor = this.audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const audioData = e.inputBuffer.getChannelData(0);
// Float32 PCM to Int16 PCM 변환
const int16Array = new Int16Array(audioData.length);
for (let i = 0; i < audioData.length; i++) {
int16Array[i] = Math.max(-32768, Math.min(32767, audioData[i] * 32768));
}
// Base64 인코딩하여 전송
const base64Audio = this.arrayBufferToBase64(int16Array.buffer);
this.ws.send(JSON.stringify({
type: 'audio_chunk',
session_id: this.sessionId,
audio_data: base64Audio,
timestamp: new Date().toISOString()
}));
}
};
source.connect(processor);
processor.connect(this.audioContext.destination);
}
arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
stop() {
if (this.ws) {
this.ws.send(JSON.stringify({
type: 'session_end',
session_id: this.sessionId
}));
this.ws.close();
}
if (this.audioContext) {
this.audioContext.close();
}
}
}
```
#### **WebSocket 실시간 수신**
```javascript
class TranscriptionWebSocket {
constructor(meetingId, onTranscription) {
this.meetingId = meetingId;
this.onTranscription = onTranscription;
this.ws = null;
}
connect() {
this.ws = new WebSocket(`ws://localhost:8080/ws/meetings/${this.meetingId}`);
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'transcription_update') {
this.onTranscription(message.data);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
// 재연결 로직
setTimeout(() => this.connect(), 3000);
};
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
```
### 5.2 Backend (Node.js + Azure Speech SDK)
#### **STT Gateway Service (WebSocket Server)**
```javascript
const WebSocket = require('ws');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const amqp = require('amqplib');
const redis = require('redis');
const wss = new WebSocket.Server({ port: 3001, path: '/api/stt/stream' });
const redisClient = redis.createClient({ url: process.env.REDIS_URL });
// Azure Speech 설정
const AZURE_SPEECH_KEY = process.env.AZURE_SPEECH_KEY;
const AZURE_SPEECH_REGION = process.env.AZURE_SPEECH_REGION; // e.g., 'koreacentral'
// 세션 저장소
const sessions = new Map();
wss.on('connection', (ws) => {
console.log('Client connected');
let recognizer = null;
let sessionId = null;
ws.on('message', async (data) => {
const message = JSON.parse(data);
try {
switch (message.type) {
case 'session_start':
sessionId = message.session_id;
await startAzureSpeechSession(ws, sessionId, message.meeting_id, message.config);
break;
case 'audio_chunk':
// 오디오 청크는 Azure Speech SDK가 자동 처리
break;
case 'session_end':
if (recognizer) {
recognizer.stopContinuousRecognitionAsync();
}
break;
}
} catch (error) {
console.error('WebSocket message error:', error);
ws.send(JSON.stringify({
type: 'error',
error: error.message
}));
}
});
ws.on('close', () => {
if (recognizer) {
recognizer.stopContinuousRecognitionAsync();
}
sessions.delete(sessionId);
console.log('Client disconnected');
});
});
// Azure Speech 세션 시작
async function startAzureSpeechSession(ws, sessionId, meetingId, config) {
// Azure Speech SDK 설정
const speechConfig = sdk.SpeechConfig.fromSubscription(
AZURE_SPEECH_KEY,
AZURE_SPEECH_REGION
);
speechConfig.speechRecognitionLanguage = config.language || 'ko-KR';
speechConfig.enableDictation();
speechConfig.setProfanity(sdk.ProfanityOption.Masked);
// 오디오 스트림 설정 (Push Stream)
const pushStream = sdk.AudioInputStream.createPushStream();
const audioConfig = sdk.AudioConfig.fromStreamInput(pushStream);
// Conversation Transcriber (화자 식별 포함)
const transcriber = new sdk.ConversationTranscriber(speechConfig, audioConfig);
// 실시간 인식 이벤트 핸들러
transcriber.transcribed = async (s, e) => {
if (e.result.reason === sdk.ResultReason.RecognizedSpeech) {
const result = {
text: e.result.text,
speaker_id: e.result.speakerId,
confidence: e.result.properties.getProperty('Confidence'),
offset: e.result.offset,
duration: e.result.duration
};
console.log(`[${result.speaker_id}]: ${result.text}`);
// 화자 매칭
const matchedUser = await matchSpeaker(meetingId, result.speaker_id);
// RabbitMQ 이벤트 발행
const event = {
event_type: 'TextTranscribed',
event_id: `EVT_${Date.now()}`,
timestamp: new Date().toISOString(),
payload: {
meeting_id: meetingId,
speaker: {
id: matchedUser?.user_id || 'Unknown',
name: matchedUser?.name || result.speaker_id,
azure_speaker_id: result.speaker_id
},
transcription: {
text: result.text,
confidence: parseFloat(result.confidence) || 0.9
},
timestamp: new Date().toISOString()
},
metadata: {
source: 'azure-speech-service',
version: '2.0'
}
};
await publishToQueue('text-transcribed', event);
// WebSocket으로 클라이언트에 실시간 전송
ws.send(JSON.stringify({
type: 'recognition_result',
session_id: sessionId,
result_id: `RESULT_${Date.now()}`,
recognition_status: 'Success',
text: result.text,
confidence: result.confidence,
speaker_id: result.speaker_id,
matched_user: matchedUser,
is_final: true,
timestamp: new Date().toISOString()
}));
}
};
// 에러 핸들러
transcriber.canceled = (s, e) => {
console.error(`Recognition canceled: ${e.errorDetails}`);
ws.send(JSON.stringify({
type: 'error',
error: e.errorDetails
}));
};
// 인식 시작
transcriber.startTranscribingAsync(() => {
console.log('Azure Speech recognition started');
ws.send(JSON.stringify({
type: 'session_ready',
session_id: sessionId
}));
// 세션 저장
sessions.set(sessionId, {
transcriber,
pushStream,
meetingId
});
});
// WebSocket에서 받은 오디오 데이터를 Push Stream에 전달
ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'audio_chunk' && message.session_id === sessionId) {
const audioBuffer = Buffer.from(message.audio_data, 'base64');
pushStream.write(audioBuffer);
}
});
}
// 화자 매칭 로직
async function matchSpeaker(meetingId, azureSpeakerId) {
// Redis에서 기존 매칭 조회
const cacheKey = `meeting:${meetingId}:speaker_mapping:${azureSpeakerId}`;
const cached = await redisClient.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 신규 화자인 경우 참석자 목록에서 추정
// TODO: 실제로는 발언 패턴, 순서 등을 분석하여 매칭
const participants = await getParticipants(meetingId);
if (participants && participants.length > 0) {
// 간단한 매칭 전략: 순서대로 할당
const speakerIndex = parseInt(azureSpeakerId.replace('Speaker_', '')) - 1;
const matchedUser = participants[speakerIndex % participants.length];
// Redis에 캐싱
await redisClient.setEx(cacheKey, 3600, JSON.stringify(matchedUser));
return matchedUser;
}
return null;
}
// 참석자 목록 조회
async function getParticipants(meetingId) {
// TODO: 실제 DB에서 조회
// 임시로 Redis에서 조회
const key = `meeting:${meetingId}:participants`;
const data = await redisClient.get(key);
return data ? JSON.parse(data) : [];
}
// RabbitMQ 발행
async function publishToQueue(queueName, message) {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
await channel.assertQueue(queueName, { durable: true });
channel.sendToQueue(queueName, Buffer.from(JSON.stringify(message)), {
persistent: true
});
await channel.close();
await connection.close();
}
console.log('Azure Speech STT Gateway running on port 3001');
```
#### **AI Service (Claude 후처리)**
```javascript
const Anthropic = require('@anthropic-ai/sdk');
const amqp = require('amqplib');
const redis = require('redis');
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
const redisClient = redis.createClient({
url: process.env.REDIS_URL
});
// RabbitMQ 구독
async function consumeQueue() {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
await channel.assertQueue('text-transcribed', { durable: true });
channel.consume('text-transcribed', async (msg) => {
const event = JSON.parse(msg.content.toString());
await processTranscription(event);
channel.ack(msg);
});
}
// Claude로 텍스트 후처리
async function processTranscription(event) {
const { meeting_id, speaker, transcription } = event.payload;
// Redis에서 기존 회의록 내용 조회
const sectionsKey = `meeting:${meeting_id}:sections:*`;
const sections = await redisClient.keys(sectionsKey);
const context = sections.length > 0
? await redisClient.get(sections[0])
: '(새로운 회의)';
// Claude API 호출
const message = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 2048,
temperature: 0.3,
system: '당신은 회의록 작성 전문가입니다. 발언 내용을 구조화하여 명확하고 간결하게 정리합니다.',
messages: [
{
role: 'user',
content: `다음은 회의 발언 내용입니다. 회의록 형식에 맞춰 정리해주세요.
발언: "${transcription.text}"
화자: ${speaker.name}
시간: ${event.payload.timestamp}
기존 회의록 내용:
${context}
템플릿 섹션: 안건, 논의 내용, 결정 사항, Todo`
}
]
});
const structuredContent = message.content[0].text;
// Redis에 업데이트된 내용 저장
await redisClient.setEx(
`meeting:${meeting_id}:sections:discussion`,
3600,
structuredContent
);
// WebSocket으로 실시간 동기화
await broadcastToWebSocket(meeting_id, {
type: 'transcription_update',
data: {
meeting_id,
speaker,
transcription: {
text: structuredContent,
is_final: true,
confidence: transcription.confidence
},
target_section: 'discussion',
action: 'append'
}
});
}
// WebSocket 브로드캐스트
async function broadcastToWebSocket(meetingId, message) {
// WebSocket 서버로 메시지 전송 (구현 필요)
// 실제로는 Redis Pub/Sub 또는 별도 WebSocket 서버 연동
}
// 서비스 시작
(async () => {
await redisClient.connect();
await consumeQueue();
console.log('AI Service started');
})();
```
---
## 6. 오류 처리 및 복구 전략
### 6.1 오류 시나리오
| 시나리오 | 감지 방법 | 대응 전략 |
|----------|-----------|-----------|
| Azure Speech 장애 | SDK error callback | 자동 재연결 (exponential backoff), 로컬 녹음 저장 |
| 네트워크 단절 | WebSocket 연결 끊김 | 자동 재연결 (최대 5회), 클라이언트 로컬 저장 |
| 낮은 confidence | score < 0.7 | 사용자에게 경고 표시, 수동 수정 권장 |
| 화자 식별 실패 | Speaker_Unknown | "미지정 화자" 표시, 수동 지정 인터페이스 제공 |
| RabbitMQ 장애 | 메시지 발행 실패 | 재시도 3회 Redis 임시 저장, 수동 복구 |
| Azure API 할당량 초과 | 429 Too Many Requests | 경고 알림, 회의 일시 중지 권장 |
### 6.2 재시도 로직
```javascript
async function retryWithExponentialBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
```
---
## 7. 성능 및 확장성
### 7.1 성능 목표
| 지표 | 목표값 | 측정 방법 |
|------|--------|-----------|
| **STT 지연 시간** | **< 1초** | 발언 시작 화면 표시 (Azure 실시간 스트리밍) |
| WebSocket 지연 | < 100ms | 메시지 발행 클라이언트 수신 |
| Claude API 응답 | < 2초 | API 호출 응답 수신 |
| 동시 회의 처리 | 100개 | Azure Speech 동시 세션 부하 테스트 |
| 화자 식별 정확도 | > 85% | Speaker Diarization 정확도 |
### 7.2 확장성 전략
- **수평 확장**: STT Gateway WebSocket 서버를 여러 인스턴스로 분산 (Load Balancer)
- **캐싱**: Redis에 화자 매칭 정보 캐싱하여 중복 처리 방지
- **Queue 파티셔닝**: 회의 ID 기반 RabbitMQ 파티셔닝
- **Azure 리소스 관리**: Azure Speech Services 할당량 모니터링 및 자동 스케일링
- **CDN**: 음성 파일 저장 시 S3 + CloudFront 활용
---
## 8. 보안 및 개인정보 보호
### 8.1 보안 요구사항
- **전송 암호화**: HTTPS/WSS 사용
- **인증/인가**: JWT 토큰 기반 회의 접근 제어
- **음성 데이터 보호**: 녹음 파일 암호화 저장 (AES-256)
- **개인정보 처리**: GDPR 준수, 음성 데이터 보관 기간 제한 (30일)
### 8.2 데이터 생명주기
```
녹음 시작 → 실시간 처리 → Redis 캐싱 (10분)
PostgreSQL 저장 (30일)
자동 삭제 (회의 종료 후 30일)
```
---
## 9. 모니터링 및 로깅
### 9.1 모니터링 지표
- **STT 성공률**: Whisper 성공률, Google 폴백 비율
- **평균 confidence score**: 텍스트 변환 품질 추적
- **처리 지연 시간**: 각 단계별 소요 시간
- **오류율**: API 오류, 네트워크 오류 비율
### 9.2 로깅 전략
```javascript
// 구조화된 로그
{
"timestamp": "2025-10-21T14:30:18.000Z",
"level": "INFO",
"service": "stt-gateway",
"event": "transcription_success",
"request_id": "REQ_001",
"meeting_id": "MTG_001",
"provider": "whisper",
"confidence": 0.95,
"processing_time_ms": 850
}
```
---
## 10. 테스트 전략
### 10.1 단위 테스트
- Azure Speech SDK 연동 모킹
- Claude API 응답 파싱
- Redis 캐싱 로직
- 화자 매칭 알고리즘
### 10.2 통합 테스트
- STT Gateway (Azure Speech) → RabbitMQ → AI Service 전체 플로우
- WebSocket 양방향 실시간 동기화
- 화자 식별 및 매칭 정확도 검증
### 10.3 성능 테스트
- 동시 100개 회의 시뮬레이션 (Azure Speech 동시 세션)
- 실시간 스트리밍 지연 시간 측정 (목표: < 1초)
- Azure API 할당량 처리량 테스트
### 10.4 품질 테스트
- 다양한 음질 환경에서 STT 정확도 측정
- 화자 식별 정확도 검증 (목표: > 85%)
- 한국어 방언 및 억양 대응 테스트
---
## 11. 구현 일정
| 단계 | 작업 | 담당자 | 예상 기간 |
|------|------|--------|-----------|
| 1 | Frontend 음성 캡처 (WebSocket) 구현 | 최유진 | 4일 |
| 2 | Azure Speech SDK 연동 및 STT Gateway 개발 | 이준호 | 6일 |
| 3 | 화자 식별 및 매칭 로직 구현 | 박서연 | 3일 |
| 4 | RabbitMQ 설정 및 이벤트 처리 | 이동욱 | 3일 |
| 5 | AI Service (Claude 연동) | 박서연 | 4일 |
| 6 | Redis 캐싱 구현 | 이준호 | 2일 |
| 7 | WebSocket 양방향 실시간 동기화 | 최유진 | 4일 |
| 8 | 통합 테스트 및 화자 식별 검증 | 정도현 | 6일 |
| 9 | 성능 최적화 및 Azure 리소스 튜닝 | 전체 | 3일 |
**총 예상 기간**: 35일 (약 5주)
---
## 12. 참고 자료
### Azure Speech Services
- [Azure Speech SDK Documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/)
- [Conversation Transcription (화자 식별)](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/conversation-transcription)
- [Real-time Speech-to-Text](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/get-started-speech-to-text)
- [Azure Speech Pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/speech-services/)
### 기타 참고 자료
- [Anthropic Claude API](https://docs.anthropic.com/claude/reference/messages_post)
- [RabbitMQ 공식 문서](https://www.rabbitmq.com/documentation.html)
- [Redis Caching Best Practices](https://redis.io/docs/manual/patterns/)
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
---
## 13. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|------|------|--------|-----------|
| 1.0 | 2025-10-21 | 개발팀 전체 | 최초 작성 (Whisper + Google 하이브리드 전략) |
| 2.0 | 2025-10-21 | 개발팀 전체 | **Azure Speech Services 단일 전략으로 전면 변경**<br>- STT 엔진: Whisper → Azure Speech Services<br>- 실시간 스트리밍 방식 적용 (지연 시간 < 1초)<br>- Speaker Diarization 기본 지원<br>- 폴백 전략 제거 (Azure 단일 사용)<br>- 구현 코드 전면 수정 (Frontend/Backend)<br>- 구현 일정 조정 (4주 → 5주) |
---
**문서 승인:**
- AI Specialist: 박서연
- Backend Developer: 이준호, 이동욱
- Frontend Developer: 최유진
- Architect: 홍길동
- QA Engineer: 정도현