dvgateway-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +615 -0
- package/dist/audio/codec.d.ts +44 -0
- package/dist/audio/codec.d.ts.map +1 -0
- package/dist/audio/codec.js +136 -0
- package/dist/audio/codec.js.map +1 -0
- package/dist/audio/codec.test.d.ts +2 -0
- package/dist/audio/codec.test.d.ts.map +1 -0
- package/dist/audio/codec.test.js +155 -0
- package/dist/audio/codec.test.js.map +1 -0
- package/dist/auth/manager.d.ts +34 -0
- package/dist/auth/manager.d.ts.map +1 -0
- package/dist/auth/manager.js +122 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/manager.test.d.ts +2 -0
- package/dist/auth/manager.test.d.ts.map +1 -0
- package/dist/auth/manager.test.js +147 -0
- package/dist/auth/manager.test.js.map +1 -0
- package/dist/client.d.ts +154 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +218 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/minutes/manager.d.ts +43 -0
- package/dist/minutes/manager.d.ts.map +1 -0
- package/dist/minutes/manager.js +71 -0
- package/dist/minutes/manager.js.map +1 -0
- package/dist/observability/logger.d.ts +19 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +70 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +143 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/metrics.test.d.ts +2 -0
- package/dist/observability/metrics.test.d.ts.map +1 -0
- package/dist/observability/metrics.test.js +122 -0
- package/dist/observability/metrics.test.js.map +1 -0
- package/dist/pipeline/builder.d.ts +111 -0
- package/dist/pipeline/builder.d.ts.map +1 -0
- package/dist/pipeline/builder.js +323 -0
- package/dist/pipeline/builder.js.map +1 -0
- package/dist/session/manager.d.ts +21 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +52 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/streams/audio-stream.d.ts +29 -0
- package/dist/streams/audio-stream.d.ts.map +1 -0
- package/dist/streams/audio-stream.js +118 -0
- package/dist/streams/audio-stream.js.map +1 -0
- package/dist/streams/call-events.d.ts +32 -0
- package/dist/streams/call-events.d.ts.map +1 -0
- package/dist/streams/call-events.js +140 -0
- package/dist/streams/call-events.js.map +1 -0
- package/dist/streams/tts-stream.d.ts +46 -0
- package/dist/streams/tts-stream.d.ts.map +1 -0
- package/dist/streams/tts-stream.js +102 -0
- package/dist/streams/tts-stream.js.map +1 -0
- package/dist/transport/http-client.d.ts +36 -0
- package/dist/transport/http-client.d.ts.map +1 -0
- package/dist/transport/http-client.js +102 -0
- package/dist/transport/http-client.js.map +1 -0
- package/dist/transport/http-client.test.d.ts +2 -0
- package/dist/transport/http-client.test.d.ts.map +1 -0
- package/dist/transport/http-client.test.js +172 -0
- package/dist/transport/http-client.test.js.map +1 -0
- package/dist/transport/ws-pool.d.ts +34 -0
- package/dist/transport/ws-pool.d.ts.map +1 -0
- package/dist/transport/ws-pool.js +123 -0
- package/dist/transport/ws-pool.js.map +1 -0
- package/dist/types/index.d.ts +378 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# DVGateway SDK
|
|
2
|
+
|
|
3
|
+
DVGateway를 통해 AI 플랫폼과 실시간 음성 서비스를 **5줄의 코드**로 연동합니다.
|
|
4
|
+
**Node.js(TypeScript)** 와 **Python** 두 언어를 모두 지원합니다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Node.js SDK
|
|
9
|
+
|
|
10
|
+
### 설치
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install dvgateway-sdk dvgateway-adapters
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 30초 시작 가이드
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { DVGatewayClient } from 'dvgateway-sdk';
|
|
20
|
+
import { DeepgramAdapter } from 'dvgateway-adapters/stt';
|
|
21
|
+
import { AnthropicAdapter } from 'dvgateway-adapters/llm';
|
|
22
|
+
import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
|
|
23
|
+
|
|
24
|
+
const gw = new DVGatewayClient({
|
|
25
|
+
baseUrl: 'http://localhost:8080', // DVGateway API 서버 (:8080)
|
|
26
|
+
auth: { type: 'apiKey', apiKey: 'your_key' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await gw.pipeline()
|
|
30
|
+
.stt(new DeepgramAdapter({ apiKey: '...', language: 'ko' }))
|
|
31
|
+
.llm(new AnthropicAdapter({ apiKey: '...', model: 'claude-sonnet-4-6' }))
|
|
32
|
+
.tts(new ElevenLabsAdapter({ apiKey: '...' }))
|
|
33
|
+
.start();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Python SDK
|
|
39
|
+
|
|
40
|
+
### 설치
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install dvgateway dvgateway-adapters python-dotenv
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 30초 시작 가이드
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
import os
|
|
51
|
+
from dotenv import load_dotenv
|
|
52
|
+
|
|
53
|
+
from dvgateway import DVGatewayClient
|
|
54
|
+
from dvgateway.adapters.stt import DeepgramAdapter
|
|
55
|
+
from dvgateway.adapters.llm import AnthropicAdapter
|
|
56
|
+
from dvgateway.adapters.tts import ElevenLabsAdapter
|
|
57
|
+
|
|
58
|
+
load_dotenv()
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
gw = DVGatewayClient(
|
|
62
|
+
base_url="http://localhost:8080",
|
|
63
|
+
auth={"type": "apiKey", "api_key": os.environ["GATEWAY_API_KEY"]},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await (
|
|
67
|
+
gw.pipeline()
|
|
68
|
+
.stt(DeepgramAdapter(api_key=os.environ["DEEPGRAM_API_KEY"], language="ko"))
|
|
69
|
+
.llm(AnthropicAdapter(api_key=os.environ["ANTHROPIC_API_KEY"], model="claude-sonnet-4-6"))
|
|
70
|
+
.tts(ElevenLabsAdapter(api_key=os.environ["ELEVENLABS_API_KEY"]))
|
|
71
|
+
.start()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
asyncio.run(main())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Python — 로컬 어댑터 (오프라인)
|
|
78
|
+
|
|
79
|
+
Python SDK는 로컬 STT/LLM 어댑터를 추가로 지원합니다. 인터넷 없이 완전 오프라인 운영이 가능합니다.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 로컬 어댑터 추가 패키지
|
|
83
|
+
pip install faster-whisper # Faster-Whisper STT
|
|
84
|
+
pip install openai-whisper # OpenAI Whisper STT (공식)
|
|
85
|
+
# Ollama는 별도 설치: https://ollama.com/install.sh
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# 완전 오프라인 파이프라인
|
|
90
|
+
from dvgateway.adapters.stt import FasterWhisperAdapter
|
|
91
|
+
from dvgateway.adapters.llm import OllamaAdapter
|
|
92
|
+
|
|
93
|
+
await (
|
|
94
|
+
gw.pipeline()
|
|
95
|
+
# Faster-Whisper: 로컬 고속 STT
|
|
96
|
+
.stt(FasterWhisperAdapter(
|
|
97
|
+
model="large-v3",
|
|
98
|
+
device="cpu", # GPU: "cuda"
|
|
99
|
+
compute_type="int8", # GPU: "float16"
|
|
100
|
+
language="ko",
|
|
101
|
+
vad_enabled=True,
|
|
102
|
+
))
|
|
103
|
+
# Qwen via Ollama: 로컬 LLM
|
|
104
|
+
.llm(OllamaAdapter(
|
|
105
|
+
base_url="http://localhost:11434",
|
|
106
|
+
model="qwen3.5:9b",
|
|
107
|
+
system_prompt="친절한 한국어 AI 상담원입니다.",
|
|
108
|
+
options={"think": False},
|
|
109
|
+
))
|
|
110
|
+
.tts(ElevenLabsAdapter(api_key="...")) # TTS는 ElevenLabs 권장
|
|
111
|
+
.start()
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Node.js — OpenAI Realtime (Speech-to-Speech)
|
|
116
|
+
|
|
117
|
+
For sub-300ms end-to-end latency, use the Realtime adapter to bypass the STT→LLM→TTS chain entirely:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { DVGatewayClient } from 'dvgateway-sdk';
|
|
121
|
+
import { OpenAIRealtimeAdapter } from 'dvgateway-adapters/realtime';
|
|
122
|
+
|
|
123
|
+
const gw = new DVGatewayClient({
|
|
124
|
+
baseUrl: 'http://localhost:8080',
|
|
125
|
+
auth: { type: 'apiKey', apiKey: 'your_key' },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const realtime = new OpenAIRealtimeAdapter({
|
|
129
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
130
|
+
model: 'gpt-4o-mini-realtime-preview', // cost-efficient; use gpt-4o-realtime-preview for best quality
|
|
131
|
+
voice: 'alloy', // alloy | echo | nova | shimmer | ash | coral | sage | verse
|
|
132
|
+
instructions: 'You are a helpful voice assistant. Keep answers short and conversational.',
|
|
133
|
+
turnDetection: { mode: 'server_vad', silenceDurationMs: 500 },
|
|
134
|
+
inputTranscription: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
realtime.onAudioOutput((chunk, linkedId) => gw.injectAudio(linkedId, chunk));
|
|
138
|
+
realtime.onTranscript((result) => console.log(`[${result.speaker}] ${result.text}`));
|
|
139
|
+
|
|
140
|
+
gw.onCallEvent(async (event) => {
|
|
141
|
+
if (event.type === 'call:new') {
|
|
142
|
+
const audioStream = gw.streamAudio(event.session.linkedId, { dir: 'in' });
|
|
143
|
+
await realtime.startSession(event.session.linkedId, audioStream);
|
|
144
|
+
}
|
|
145
|
+
if (event.type === 'call:ended') {
|
|
146
|
+
await realtime.stop(event.linkedId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await gw.connect();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Realtime vs cascaded pipeline:**
|
|
154
|
+
|
|
155
|
+
| | Cascaded (STT→LLM→TTS) | Realtime (Speech-to-Speech) |
|
|
156
|
+
|--|---|---|
|
|
157
|
+
| Latency | ~500ms | ~300ms |
|
|
158
|
+
| Cost | per-service billing | single API call |
|
|
159
|
+
| Control | full per-step control | unified session |
|
|
160
|
+
| Best for | complex agents, custom logic | low-latency voice bots |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### 사람다운 음성 최적화 (Human Voice Options)
|
|
167
|
+
|
|
168
|
+
TTS 어댑터에 **사람다운 음성** 옵션이 내장되어 있습니다. 기본값은 **한국어 대화**에 최적화되어 있으며, 자연스러운 쉼, 숨소리, 필러 단어, 감탄사, 감정 표현을 제어할 수 있습니다.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
|
|
172
|
+
import { OpenAITtsAdapter } from 'dvgateway-adapters/tts';
|
|
173
|
+
|
|
174
|
+
// ✅ 기본값: 한국어 최적화 (humanVoice 자동 활성화)
|
|
175
|
+
const tts = new ElevenLabsAdapter({ apiKey: '...' });
|
|
176
|
+
// → stability: 0.3, style: 0.6, model: eleven_multilingual_v2
|
|
177
|
+
|
|
178
|
+
// ✅ OpenAI: gpt-4o-mini-tts 자동 선택 + 음성 지시 자동 생성
|
|
179
|
+
const openaiTts = new OpenAITtsAdapter({ apiKey: '...' });
|
|
180
|
+
// → model: gpt-4o-mini-tts, voiceInstructions 자동 생성
|
|
181
|
+
|
|
182
|
+
// ✅ 커스텀 설정
|
|
183
|
+
const customTts = new ElevenLabsAdapter({
|
|
184
|
+
apiKey: '...',
|
|
185
|
+
humanVoice: {
|
|
186
|
+
naturalPauses: true, // 문장 사이 자연스러운 쉼
|
|
187
|
+
breathingSounds: true, // 숨소리 포함
|
|
188
|
+
fillerWords: false, // 필러 단어 비활성화 (음, 어, 그...)
|
|
189
|
+
exclamations: true, // 감탄사 (아, 네, 맞아요...)
|
|
190
|
+
emotionalRange: 0.8, // 감정 표현 범위 (0.0–1.0)
|
|
191
|
+
speechVariation: 0.7, // 음성 변화도 (0.0–1.0)
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ❌ 비활성화: 기존 방식 사용
|
|
196
|
+
const flatTts = new ElevenLabsAdapter({
|
|
197
|
+
apiKey: '...',
|
|
198
|
+
humanVoice: false,
|
|
199
|
+
// → stability: 0.5, style: 0.0, model: eleven_flash_v2_5 (기존 기본값)
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### HumanVoiceOptions 인터페이스
|
|
204
|
+
|
|
205
|
+
| 옵션 | 타입 | 기본값 (KO) | 설명 |
|
|
206
|
+
|------|------|-------------|------|
|
|
207
|
+
| `naturalPauses` | `boolean` | `true` | 문장·절 사이 자연스러운 쉼 |
|
|
208
|
+
| `breathingSounds` | `boolean` | `true` | 긴 문장 사이 숨소리 |
|
|
209
|
+
| `fillerWords` | `boolean` | `true` | 필러 단어 (음, 어, 그, 저) |
|
|
210
|
+
| `exclamations` | `boolean` | `true` | 감탄사 (아, 네, 맞아요) |
|
|
211
|
+
| `emotionalRange` | `number` | `0.6` | 감정 표현 범위 (0.0–1.0) |
|
|
212
|
+
| `speechVariation` | `number` | `0.7` | 음성 변화도 (0.0–1.0) |
|
|
213
|
+
|
|
214
|
+
#### 프로바이더별 매핑
|
|
215
|
+
|
|
216
|
+
| HumanVoiceOptions | ElevenLabs | OpenAI (gpt-4o-mini-tts) |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `emotionalRange` | → `style` 파라미터 | → voiceInstructions 감정 지시 |
|
|
219
|
+
| `speechVariation` | → `stability` (역비례: 1.0 - variation) | → voiceInstructions 톤 변화 지시 |
|
|
220
|
+
| `naturalPauses` | 모델 내장 (multilingual v2) | → voiceInstructions 쉼 지시 |
|
|
221
|
+
| `breathingSounds` | 모델 내장 (multilingual v2) | → voiceInstructions 숨소리 지시 |
|
|
222
|
+
| `fillerWords` | 모델 내장 | → voiceInstructions 필러 단어 지시 |
|
|
223
|
+
| `exclamations` | 모델 내장 | → voiceInstructions 감탄사 지시 |
|
|
224
|
+
|
|
225
|
+
#### 프리셋
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { HUMAN_VOICE_DEFAULTS_KO, HUMAN_VOICE_DEFAULTS_EN } from 'dvgateway-sdk';
|
|
229
|
+
|
|
230
|
+
// 한국어 기본값 (기본 적용)
|
|
231
|
+
HUMAN_VOICE_DEFAULTS_KO // { naturalPauses: true, breathingSounds: true, fillerWords: true, ... }
|
|
232
|
+
|
|
233
|
+
// 영어 기본값
|
|
234
|
+
HUMAN_VOICE_DEFAULTS_EN // { naturalPauses: true, breathingSounds: true, fillerWords: false, ... }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### ElevenLabs 한국어 네이티브 보이스
|
|
240
|
+
|
|
241
|
+
ElevenLabs Voice Library에서 선별한 **9개 한국어 네이티브 음성**이 내장되어 있습니다:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { ELEVENLABS_KOREAN_VOICES } from 'dvgateway-adapters';
|
|
245
|
+
|
|
246
|
+
// 내장 한국어 음성 목록
|
|
247
|
+
for (const voice of ELEVENLABS_KOREAN_VOICES) {
|
|
248
|
+
console.log(`${voice.id} — ${voice.label}`);
|
|
249
|
+
}
|
|
250
|
+
// pjJMvFj0JGWi3mogOkHH — Hyun Bin (남성, 한국어)
|
|
251
|
+
// t0jbNlBVZ17f02VDIeMI — 지영 / JiYoung (여성, 한국어)
|
|
252
|
+
// zrHiDhphv9ZnVXBqCLjz — Jennie (여성, 한국어)
|
|
253
|
+
// ZJCNdOEhQGMOIbMuhBME — Han Aim (남성, 한국어)
|
|
254
|
+
// ova4yY2jqnnUdGOmTGbx — KKC HQ (남성, 한국어)
|
|
255
|
+
// Xb7hH8MSUJpSbSDYk0k2 — Anna Kim (여성, 한국어)
|
|
256
|
+
// XrExE9yKIg1WjnnlVkGX — Yuna (여성, 한국어)
|
|
257
|
+
// ThT5KcBeYPX3keUQqHPh — Jina (여성, 한국어)
|
|
258
|
+
// Sita5M0jWFxPiECPABjR — jjeong (여성, 한국어)
|
|
259
|
+
|
|
260
|
+
// 한국어 음성으로 TTS 생성
|
|
261
|
+
const tts = new ElevenLabsAdapter({
|
|
262
|
+
apiKey: '...',
|
|
263
|
+
voiceId: ELEVENLABS_KOREAN_VOICES[0].id, // Hyun Bin
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
# Python
|
|
269
|
+
from dvgateway.adapters.tts import ElevenLabsAdapter, KOREAN_VOICES
|
|
270
|
+
|
|
271
|
+
for voice in KOREAN_VOICES:
|
|
272
|
+
print(f"{voice.id} — {voice.label}")
|
|
273
|
+
|
|
274
|
+
tts = ElevenLabsAdapter(api_key="...", voice_id=KOREAN_VOICES[0].id)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### ElevenLabs Voice Fetch (동적 음성 목록 조회)
|
|
280
|
+
|
|
281
|
+
ElevenLabs 계정에 등록된 **모든 음성**(기본, 클론, 라이브러리)을 동적으로 조회합니다:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
|
|
285
|
+
|
|
286
|
+
// API에서 사용 가능한 모든 음성 조회
|
|
287
|
+
const voices = await ElevenLabsAdapter.fetchVoices('your-elevenlabs-api-key');
|
|
288
|
+
for (const v of voices) {
|
|
289
|
+
console.log(`${v.id} — ${v.label}`);
|
|
290
|
+
// 클론된 음성: "My Voice (클론) [ko]"
|
|
291
|
+
// 생성된 음성: "Custom Voice (생성됨)"
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# Python
|
|
297
|
+
voices = await ElevenLabsAdapter.fetch_voices("your-elevenlabs-api-key")
|
|
298
|
+
for v in voices:
|
|
299
|
+
print(f"{v.id} — {v.label}")
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**DVGateway REST API:**
|
|
303
|
+
```
|
|
304
|
+
GET /api/v1/config/apikeys/voices/elevenlabs/fetch
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### ElevenLabs Voice Cloning (음성 복제)
|
|
310
|
+
|
|
311
|
+
오디오 파일을 업로드하여 **커스텀 음성을 생성**합니다:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { ElevenLabsAdapter } from 'dvgateway-adapters/tts';
|
|
315
|
+
import { readFileSync } from 'fs';
|
|
316
|
+
|
|
317
|
+
// 오디오 파일에서 음성 복제
|
|
318
|
+
const audioData = readFileSync('./my-voice-sample.wav');
|
|
319
|
+
const newVoice = await ElevenLabsAdapter.cloneVoice(
|
|
320
|
+
'your-elevenlabs-api-key',
|
|
321
|
+
'내 목소리', // 음성 이름
|
|
322
|
+
audioData, // 오디오 데이터 (WAV, MP3, OGG)
|
|
323
|
+
'my-voice-sample.wav', // 파일명
|
|
324
|
+
'한국어 남성 음성', // 설명 (선택)
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
console.log(`클론된 음성 ID: ${newVoice.id}`); // → 새로운 voice_id
|
|
328
|
+
|
|
329
|
+
// 클론된 음성으로 TTS 생성
|
|
330
|
+
const tts = new ElevenLabsAdapter({
|
|
331
|
+
apiKey: '...',
|
|
332
|
+
voiceId: newVoice.id,
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
# Python
|
|
338
|
+
audio_data = open("./my-voice-sample.wav", "rb").read()
|
|
339
|
+
new_voice = await ElevenLabsAdapter.clone_voice(
|
|
340
|
+
api_key="your-elevenlabs-api-key",
|
|
341
|
+
name="내 목소리",
|
|
342
|
+
audio_data=audio_data,
|
|
343
|
+
file_name="my-voice-sample.wav",
|
|
344
|
+
description="한국어 남성 음성",
|
|
345
|
+
)
|
|
346
|
+
print(f"클론된 음성 ID: {new_voice.id}")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**DVGateway REST API:**
|
|
350
|
+
```
|
|
351
|
+
POST /api/v1/config/apikeys/voices/elevenlabs/clone
|
|
352
|
+
Content-Type: multipart/form-data
|
|
353
|
+
Fields: name, description (optional), file (audio)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
### TTS 캐싱 (CachedTtsAdapter)
|
|
359
|
+
|
|
360
|
+
디스크 기반 TTS 오디오 캐싱으로 **비용 절감**과 **응답 속도 향상**을 동시에 달성합니다:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { CachedTtsAdapter, ElevenLabsAdapter } from 'dvgateway-adapters';
|
|
364
|
+
|
|
365
|
+
const inner = new ElevenLabsAdapter({ apiKey: '...' });
|
|
366
|
+
const tts = new CachedTtsAdapter(inner, {
|
|
367
|
+
provider: 'elevenlabs',
|
|
368
|
+
cacheDir: './tts-cache',
|
|
369
|
+
ttlMs: 7 * 24 * 60 * 60 * 1000, // 7일
|
|
370
|
+
maxEntries: 1000, // LRU 제한
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// 자주 사용하는 안내 멘트 사전 캐싱
|
|
374
|
+
await tts.warmup([
|
|
375
|
+
{ text: '안녕하세요, 무엇을 도와드릴까요?' },
|
|
376
|
+
{ text: '잠시만 기다려 주세요.' },
|
|
377
|
+
{ text: '감사합니다. 좋은 하루 되세요.' },
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
// 캐시 히트 시 즉시 응답 (API 호출 없음)
|
|
381
|
+
const stats = tts.getStats();
|
|
382
|
+
console.log(`캐시 히트: ${stats.hits}, 미스: ${stats.misses}`);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
# Python
|
|
387
|
+
from dvgateway.adapters.tts import CachedTtsAdapter, ElevenLabsAdapter
|
|
388
|
+
|
|
389
|
+
inner = ElevenLabsAdapter(api_key="...")
|
|
390
|
+
tts = CachedTtsAdapter(inner, provider="elevenlabs", cache_dir="./tts-cache")
|
|
391
|
+
|
|
392
|
+
await tts.warmup([
|
|
393
|
+
{"text": "안녕하세요, 무엇을 도와드릴까요?"},
|
|
394
|
+
{"text": "잠시만 기다려 주세요."},
|
|
395
|
+
])
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### STT 옵션 (SttOptions)
|
|
401
|
+
|
|
402
|
+
STT 어댑터에 전달 가능한 프로바이더 독립적 옵션입니다.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import type { SttOptions } from 'dvgateway-sdk';
|
|
406
|
+
|
|
407
|
+
const sttOptions: SttOptions = {
|
|
408
|
+
language: 'ko', // 언어 코드
|
|
409
|
+
diarize: true, // 화자 분리
|
|
410
|
+
vadSensitivity: 'medium', // VAD 감도 (low/medium/high)
|
|
411
|
+
endpointingMs: 300, // 발화 종료 감지 (ms)
|
|
412
|
+
utteranceEndMs: 800, // 발화 최종 확정 (ms, 한국어 최적화)
|
|
413
|
+
interimResults: true, // 중간 결과 수신
|
|
414
|
+
smartFormat: true, // 스마트 포맷팅 (숫자, 날짜, 구두점)
|
|
415
|
+
keywords: ['DVGateway', 'AI'], // 도메인 키워드 부스트
|
|
416
|
+
punctuate: true, // 구두점 자동 추가
|
|
417
|
+
profanityFilter: false, // 비속어 필터
|
|
418
|
+
sentiment: true, // 감정 분석 (Deepgram Nova-3)
|
|
419
|
+
};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### 감정 분석 (Sentiment Analysis)
|
|
425
|
+
|
|
426
|
+
Deepgram Nova-3 모델에서 **실시간 감정 분석**을 지원합니다. 각 발화(transcript segment)를 `positive` / `neutral` / `negative`로 분류하고 신뢰도 점수를 반환합니다.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { DeepgramAdapter } from 'dvgateway-adapters/stt';
|
|
430
|
+
|
|
431
|
+
// sentiment: true 로 감정 분석 활성화
|
|
432
|
+
const stt = new DeepgramAdapter({
|
|
433
|
+
apiKey: '...',
|
|
434
|
+
language: 'ko',
|
|
435
|
+
sentiment: true, // 감정 분석 활성화
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
gw.pipeline()
|
|
439
|
+
.stt(stt)
|
|
440
|
+
.onTranscript((result, session) => {
|
|
441
|
+
console.log(`[${result.speaker}] ${result.text}`);
|
|
442
|
+
if (result.sentiment) {
|
|
443
|
+
console.log(` 감정: ${result.sentiment.sentiment} (${result.sentiment.sentimentScore})`);
|
|
444
|
+
// → 감정: positive (0.87)
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
.start();
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
# Python
|
|
452
|
+
from dvgateway.adapters.stt import DeepgramAdapter
|
|
453
|
+
|
|
454
|
+
stt = DeepgramAdapter(api_key="...", language="ko", sentiment=True)
|
|
455
|
+
|
|
456
|
+
# result.sentiment.sentiment → "positive" | "neutral" | "negative"
|
|
457
|
+
# result.sentiment.sentiment_score → 0.0–1.0
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
#### SentimentResult 인터페이스
|
|
461
|
+
|
|
462
|
+
| 필드 | 타입 | 설명 |
|
|
463
|
+
|------|------|------|
|
|
464
|
+
| `sentiment` | `'positive' \| 'neutral' \| 'negative'` | 세그먼트 감정 분류 |
|
|
465
|
+
| `sentimentScore` | `number` | 감정 신뢰도 점수 (0.0–1.0) |
|
|
466
|
+
|
|
467
|
+
> 대시보드에서도 Deepgram STT 프로바이더 설정에서 "감정 분석" 체크박스로 활성화할 수 있습니다.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### Node.js vs Python SDK 비교
|
|
472
|
+
|
|
473
|
+
| 항목 | Node.js | Python |
|
|
474
|
+
|------|---------|--------|
|
|
475
|
+
| 패키지 | `dvgateway-sdk` | `dvgateway` |
|
|
476
|
+
| 어댑터 | `dvgateway-adapters` | `dvgateway-adapters` |
|
|
477
|
+
| 이벤트 핸들러 | `.onNewCall(cb)` | `.on_new_call(cb)` |
|
|
478
|
+
| 비동기 | `async/await` | `asyncio` |
|
|
479
|
+
| Realtime (S2S) | ✅ `OpenAIRealtimeAdapter` | ✅ `OpenAIRealtimeAdapter` |
|
|
480
|
+
| 로컬 STT 지원 | whisper.cpp (외부 서버) | ✅ whisper.cpp, Faster-Whisper, OpenAI Whisper (인프로세스) |
|
|
481
|
+
| 로컬 LLM 지원 | Ollama (외부 서버) | ✅ Ollama, vLLM (OpenAI 호환) |
|
|
482
|
+
|
|
483
|
+
## DVGateway 포트 구조
|
|
484
|
+
|
|
485
|
+
| 포트 | 용도 |
|
|
486
|
+
|------|------|
|
|
487
|
+
| `:8080` | API 서버 — AI SDK가 연결하는 포트 |
|
|
488
|
+
| `:8081` | 대시보드 UI |
|
|
489
|
+
| `:8092` | 미디어 서버 — Asterisk ExternalMedia WebSocket (`GW_MEDIA_ADDR` 기본값) |
|
|
490
|
+
| `:8088` | Asterisk ARI HTTP — DVGateway가 Asterisk에 연결할 때 사용 |
|
|
491
|
+
|
|
492
|
+
> ⚠️ SDK는 항상 **:8080 API 서버**에 연결합니다. `:8092`는 Asterisk가 DVGateway에 연결하는 포트입니다.
|
|
493
|
+
|
|
494
|
+
## 아키텍처
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
Asterisk PBX
|
|
498
|
+
│ ExternalMedia WebSocket (slin16, 16kHz)
|
|
499
|
+
↓
|
|
500
|
+
DVGateway (:8092 미디어 서버)
|
|
501
|
+
│
|
|
502
|
+
↓
|
|
503
|
+
DVGateway API (:8080)
|
|
504
|
+
│ ├── /api/v1/ws/callinfo 콜 이벤트
|
|
505
|
+
│ ├── /api/v1/ws/stream 오디오 스트림 (AI → 구독)
|
|
506
|
+
│ └── /api/v1/ws/tts/{id} TTS 주입 (AI → Asterisk)
|
|
507
|
+
│
|
|
508
|
+
↓
|
|
509
|
+
dvgateway-sdk (이 패키지)
|
|
510
|
+
│
|
|
511
|
+
├── SttAdapter (Deepgram, whisper.cpp, Faster-Whisper, ...)
|
|
512
|
+
├── LlmAdapter (Anthropic Claude, OpenAI GPT, Ollama/Qwen, ...)
|
|
513
|
+
├── TtsAdapter (ElevenLabs, OpenAI TTS, ...)
|
|
514
|
+
└── RealtimeAdapter (OpenAI Realtime — speech-to-speech, bypasses STT/LLM/TTS)
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## 오디오 포맷
|
|
518
|
+
|
|
519
|
+
DVGateway는 **slin16** 포맷으로 오디오를 전송합니다:
|
|
520
|
+
- 샘플레이트: 16,000 Hz
|
|
521
|
+
- 비트깊이: 16-bit signed integer
|
|
522
|
+
- 채널: Mono (1채널)
|
|
523
|
+
- 엔디안: Little-endian
|
|
524
|
+
- 프레임 크기: 640 bytes = 320 samples = **20ms**
|
|
525
|
+
|
|
526
|
+
SDK는 자동으로 `slin16 → Float32Array [-1.0, 1.0]`으로 변환합니다.
|
|
527
|
+
|
|
528
|
+
## API 참조
|
|
529
|
+
|
|
530
|
+
### 고수준 API (파이프라인 빌더)
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
gw.pipeline()
|
|
534
|
+
.stt(adapter) // STT 어댑터 설정
|
|
535
|
+
.llm(adapter) // LLM 어댑터 설정 (선택)
|
|
536
|
+
.tts(adapter) // TTS 어댑터 설정 (선택)
|
|
537
|
+
.audioFilter({ dir: 'in' }) // 오디오 방향 필터 (both/in/out)
|
|
538
|
+
.onNewCall(handler) // 새 콜 이벤트
|
|
539
|
+
.onCallEnded(handler) // 콜 종료 이벤트
|
|
540
|
+
.onTranscript(handler) // 전사 결과 이벤트
|
|
541
|
+
.onError(handler) // 오류 이벤트
|
|
542
|
+
.start() // 시작 (Promise — 중단할 때까지 실행)
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 중간수준 API
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
// 오디오 스트림 구독
|
|
549
|
+
const stream = gw.streamAudio(linkedId, { dir: 'both' });
|
|
550
|
+
for await (const chunk of stream) {
|
|
551
|
+
// chunk.samples: Float32Array
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// TTS 주입
|
|
555
|
+
await gw.injectTts(linkedId, tts.synthesize('안녕하세요'));
|
|
556
|
+
|
|
557
|
+
// 컨퍼런스 TTS 브로드캐스트
|
|
558
|
+
await gw.broadcastTts(confId, tts.synthesize('안내말씀'));
|
|
559
|
+
|
|
560
|
+
// 콜 이벤트 구독
|
|
561
|
+
const unsub = gw.onCallEvent((event) => { /* ... */ });
|
|
562
|
+
|
|
563
|
+
// 세션 관리
|
|
564
|
+
const sessions = await gw.listSessions();
|
|
565
|
+
await gw.updateSessionMeta(linkedId, { customerId: 'C001' });
|
|
566
|
+
|
|
567
|
+
// 회의록
|
|
568
|
+
await gw.submitTranscript(confId, result);
|
|
569
|
+
const minutes = await gw.downloadMinutes(confId, 'txt');
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## 보안
|
|
573
|
+
|
|
574
|
+
- **API Key → JWT 자동 교환**: SDK가 내부적으로 처리, 사용자 코드에서 토큰 관리 불필요
|
|
575
|
+
- **PII 자동 마스킹**: 로그에서 전화번호 등 개인정보 자동 제거 (기본 활성화)
|
|
576
|
+
- **TLS 강제**: `http://`를 `https://`로 자동 업그레이드 (프로덕션)
|
|
577
|
+
- **mTLS 지원**: 기업 환경의 클라이언트 인증서 지원
|
|
578
|
+
|
|
579
|
+
## 서비스 지속성
|
|
580
|
+
|
|
581
|
+
- **자동 재연결**: WebSocket 끊김 시 지수 백오프(Exponential backoff)로 자동 재연결
|
|
582
|
+
- **활성 콜 보호**: 재연결 후 진행 중인 콜 자동 재구독
|
|
583
|
+
- **재연결 설정**:
|
|
584
|
+
```typescript
|
|
585
|
+
new DVGatewayClient({
|
|
586
|
+
reconnect: {
|
|
587
|
+
maxAttempts: 10, // 최대 재시도 횟수
|
|
588
|
+
initialDelayMs: 2000, // 첫 재시도 대기 (2초)
|
|
589
|
+
maxDelayMs: 30_000, // 최대 대기 (30초)
|
|
590
|
+
backoffMultiplier: 2.0, // 대기 시간 배수
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## 메트릭
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// 레이턴시 통계
|
|
599
|
+
gw.metrics.logSummary();
|
|
600
|
+
|
|
601
|
+
// Prometheus 형식으로 내보내기
|
|
602
|
+
const promText = gw.metrics.toPrometheus();
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
수집 메트릭:
|
|
606
|
+
- `dvgw_stt_latency_ms` — STT 응답 시간
|
|
607
|
+
- `dvgw_llm_ttft_ms` — LLM 첫 토큰까지 시간
|
|
608
|
+
- `dvgw_tts_latency_ms` — TTS 첫 오디오까지 시간
|
|
609
|
+
- `dvgw_e2e_latency_ms` — 전체 E2E 레이턴시
|
|
610
|
+
- `dvgw_transcripts_total` — 처리된 발화 수
|
|
611
|
+
- `dvgw_errors_total` — 오류 수
|
|
612
|
+
|
|
613
|
+
## 라이선스
|
|
614
|
+
|
|
615
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Codec Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts between DVGateway's native slin16 format and Float32 PCM
|
|
5
|
+
* used by most AI STT/TTS APIs.
|
|
6
|
+
*
|
|
7
|
+
* DVGateway audio format:
|
|
8
|
+
* - Sample rate: 16000 Hz
|
|
9
|
+
* - Bit depth: 16-bit signed integer
|
|
10
|
+
* - Endianness: little-endian
|
|
11
|
+
* - Frame size: 640 bytes = 320 samples = 20ms
|
|
12
|
+
* - Channels: 1 (mono)
|
|
13
|
+
*/
|
|
14
|
+
/** Convert slin16 Buffer → normalized Float32Array [-1.0, 1.0] */
|
|
15
|
+
export declare function slin16ToFloat32(buf: Buffer): Float32Array;
|
|
16
|
+
/** Convert normalized Float32Array [-1.0, 1.0] → slin16 Buffer */
|
|
17
|
+
export declare function float32ToSlin16(samples: Float32Array): Buffer;
|
|
18
|
+
/** Convert μ-law (ulaw) Buffer → slin16 Buffer */
|
|
19
|
+
export declare function ulawToSlin16(ulaw: Buffer): Buffer;
|
|
20
|
+
/** Convert slin16 Buffer → μ-law (ulaw) Buffer */
|
|
21
|
+
export declare function slin16ToUlaw(slin16: Buffer): Buffer;
|
|
22
|
+
/**
|
|
23
|
+
* Resample Float32Array from one sample rate to another.
|
|
24
|
+
* Uses linear interpolation — suitable for real-time processing.
|
|
25
|
+
*
|
|
26
|
+
* @param samples - Input samples
|
|
27
|
+
* @param fromRate - Source sample rate (e.g. 16000)
|
|
28
|
+
* @param toRate - Target sample rate (e.g. 24000 for ElevenLabs)
|
|
29
|
+
*/
|
|
30
|
+
export declare function resample(samples: Float32Array, fromRate: number, toRate: number): Float32Array;
|
|
31
|
+
/**
|
|
32
|
+
* Calculate RMS (Root Mean Square) amplitude of audio samples.
|
|
33
|
+
* Returns a value in [0.0, 1.0].
|
|
34
|
+
*/
|
|
35
|
+
export declare function calculateRms(samples: Float32Array): number;
|
|
36
|
+
/**
|
|
37
|
+
* Simple Voice Activity Detection based on RMS threshold.
|
|
38
|
+
* Returns true if the frame likely contains speech.
|
|
39
|
+
*
|
|
40
|
+
* @param samples - Float32 samples
|
|
41
|
+
* @param threshold - RMS threshold (default: 0.01, i.e. -40 dBFS)
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectVoiceActivity(samples: Float32Array, threshold?: number): boolean;
|
|
44
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/audio/codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAYzD;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAW7D;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASjD;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUnD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,YAAY,CAkBd;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAS1D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,SAAO,GAAG,OAAO,CAEpF"}
|