# 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 ๋จ์ผ ์ ๋ต์ผ๋ก ์ ๋ฉด ๋ณ๊ฒฝ**
- STT ์์ง: Whisper โ Azure Speech Services
- ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์ ์ ์ฉ (์ง์ฐ ์๊ฐ < 1์ด)
- Speaker Diarization ๊ธฐ๋ณธ ์ง์
- ํด๋ฐฑ ์ ๋ต ์ ๊ฑฐ (Azure ๋จ์ผ ์ฌ์ฉ)
- ๊ตฌํ ์ฝ๋ ์ ๋ฉด ์์ (Frontend/Backend)
- ๊ตฌํ ์ผ์ ์กฐ์ (4์ฃผ โ 5์ฃผ) |
---
**๋ฌธ์ ์น์ธ:**
- AI Specialist: ๋ฐ์์ฐ
- Backend Developer: ์ด์คํธ, ์ด๋์ฑ
- Frontend Developer: ์ต์ ์ง
- Architect: ํ๊ธธ๋
- QA Engineer: ์ ๋ํ