2024년 STT 기술 동향
24.09.01
2024년 STT 기술 동향
STT(Speech-to-Text) 분야에서 일하다 보니 기술 트렌드를 계속 쫓게 됩니다. 2024년 현재 이 분야에서 무엇이 달라졌는지 정리합니다.
가장 큰 변화: 엣지로의 이동
클라우드 STT에서 온디바이스 STT로 흐름이 바뀌고 있습니다.
# 2024년 STT 아키텍처 트렌드
STT_ARCHITECTURE_TRENDS = {
"2020-2022": {
"paradigm": "Cloud-First",
"approach": "오디오를 서버로 전송 → 추론 → 결과 반환",
"latency": "200-500ms (네트워크 포함)",
"pros": ["높은 정확도", "복잡한 모델 가능"],
"cons": ["프라이버시 우려", "네트워크 의존", "API 비용"]
},
"2023-2024": {
"paradigm": "Edge-First",
"approach": "디바이스에서 직접 추론",
"latency": "30-100ms",
"pros": ["프라이버시 보장", "오프라인 가능", "무료"],
"cons": ["디바이스 성능 제약", "모델 크기 제한"]
}
}WhisperKit이 대표적입니다. 아이폰에서 Whisper 모델을 실시간으로 돌립니다. 처음 봤을 때 놀랐습니다. 10억 파라미터짜리 모델이 폰에서 돌아가다니.
// WhisperKit 사용 예시 (Swift)
import WhisperKit
class SpeechRecognizer {
private var whisperKit: WhisperKit?
init() async throws {
// 모델 로드 (최초 실행 시 다운로드)
whisperKit = try await WhisperKit(
model: "base", // tiny, base, small 등
computeOptions: .init(
melCompute: .cpuAndGPU, // Neural Engine 활용
audioEncoderCompute: .cpuAndGPU,
textDecoderCompute: .cpuAndGPU
)
)
}
func transcribe(audioURL: URL) async throws -> String {
guard let whisperKit = whisperKit else {
throw RecognitionError.modelNotLoaded
}
let results = try await whisperKit.transcribe(
audioPath: audioURL.path,
decodeOptions: .init(
language: "ko", // 한국어
task: .transcribe,
temperature: 0.0, // greedy decoding
sampleLength: 224
)
)
return results.map { $0.text }.joined(separator: " ")
}
// 실시간 스트리밍 처리
func streamTranscribe(audioBuffer: AVAudioPCMBuffer) async throws -> String {
// 청크 단위로 처리
let result = try await whisperKit?.transcribe(
audioArray: audioBuffer.floatChannelData![0],
decodeOptions: .init(
language: "ko",
task: .transcribe
)
)
return result?.first?.text ?? ""
}
}
// 성능 벤치마크 (iPhone 15 Pro)
WHISPERKIT_PERFORMANCE = {
"tiny": {"model_size": "39MB", "rtf": "0.03", "wer": "14.2%"},
"base": {"model_size": "74MB", "rtf": "0.05", "wer": "10.1%"},
"small": {"model_size": "244MB", "rtf": "0.12", "wer": "7.3%"}
}왜 엣지로 가느냐? 세 가지 이유가 있습니다.
EDGE_STT_DRIVERS = {
"privacy": {
"concern": "음성 데이터는 매우 민감한 개인정보",
"regulation": "GDPR, 개인정보보호법 강화",
"solution": "데이터가 디바이스를 떠나지 않음"
},
"latency": {
"cloud_latency": "네트워크 왕복 100-300ms",
"edge_latency": "추론만 30-50ms",
"impact": "실시간 자막, 음성 비서에 필수"
},
"cost": {
"cloud_cost": "$0.006-0.024 per minute (대형 서비스)",
"edge_cost": "$0 (초기 개발 비용만)",
"scale": "월 1억 분 처리 시 연 $7M+ 절감 가능"
}
}FastConformer: 새로운 표준
NVIDIA에서 나온 FastConformer가 인상적입니다. 기존 Conformer보다 훨씬 효율적입니다.
# FastConformer vs Conformer 비교
import torch
import torch.nn as nn
class ConformerAttention(nn.Module):
"""
기존 Conformer의 Multi-Head Self-Attention
복잡도: O(T²) where T = sequence length
"""
def forward(self, x):
# Full attention over all positions
T = x.size(1)
attn = torch.matmul(x, x.transpose(-2, -1)) # [B, T, T]
# T=1000이면 1M 연산
return attn
class FastConformerAttention(nn.Module):
"""
FastConformer의 Linear Attention
복잡도: O(T)
핵심 변경:
1. Downsampling으로 시퀀스 길이 감소
2. Linear attention approximation
3. Cached inference for streaming
"""
def __init__(self, d_model, n_heads, downsample_factor=8):
super().__init__()
self.downsample = nn.Conv1d(
d_model, d_model,
kernel_size=downsample_factor,
stride=downsample_factor
)
self.attention = nn.MultiheadAttention(d_model, n_heads)
self.upsample = nn.ConvTranspose1d(
d_model, d_model,
kernel_size=downsample_factor,
stride=downsample_factor
)
def forward(self, x):
# [B, T, D] -> [B, D, T]
x = x.transpose(1, 2)
# Downsample: T -> T/8
x_down = self.downsample(x)
# Attention on reduced sequence
x_down = x_down.transpose(1, 2)
attn_out, _ = self.attention(x_down, x_down, x_down)
# Upsample back: T/8 -> T
x_up = self.upsample(attn_out.transpose(1, 2))
return x_up.transpose(1, 2)
# 실제 성능 차이 (30초 오디오 기준)
PERFORMANCE_COMPARISON = {
"Conformer-Large": {
"params": "121M",
"memory": "8.2GB",
"latency": "420ms",
"WER_librispeech": "2.1%"
},
"FastConformer-Large": {
"params": "115M",
"memory": "4.1GB", # 50% 감소
"latency": "180ms", # 57% 감소
"WER_librispeech": "2.0%" # 오히려 향상
}
}핵심은 attention 복잡도를 O(n²)에서 O(n)으로 줄인 것입니다. 긴 오디오를 처리할 때 차이가 큽니다.
# NeMo를 이용한 FastConformer 사용
import nemo.collections.asr as nemo_asr
# 사전학습된 FastConformer 로드
model = nemo_asr.models.EncDecCTCModelBPE.from_pretrained(
"nvidia/parakeet-ctc-1.1b" # 1.1B 파라미터 모델
)
# 추론
transcription = model.transcribe(["audio.wav"])
print(transcription)
# 스트리밍 설정
streaming_config = {
"chunk_size": 1.6, # 1.6초 청크
"shift_size": 0.4, # 0.4초 오버랩
"lookahead": 0, # 실시간을 위해 lookahead 없음
}
# 스트리밍 추론
class StreamingASR:
def __init__(self, model):
self.model = model
self.buffer = []
self.cache = None
def process_chunk(self, audio_chunk):
"""
청크 단위 처리 (스트리밍)
"""
self.buffer.append(audio_chunk)
if len(self.buffer) * self.chunk_size >= 1.6:
# 충분한 컨텍스트 쌓이면 추론
audio = torch.cat(self.buffer[-4:], dim=-1) # 최근 4청크
with torch.no_grad():
result, self.cache = self.model.transcribe_chunk(
audio,
cache=self.cache
)
return result
return None우리 팀에서도 FastConformer 기반으로 STT 엔진을 만들었는데, 기존 대비 지연시간이 48% 줄었습니다. 메모리도 거의 절반으로 줄었습니다.
양자화 기술의 성숙
INT8 양자화가 거의 표준이 되었습니다. 모델 크기를 4배 줄이면서 정확도 손실은 1% 미만입니다.
import torch
from torch.quantization import quantize_dynamic
import onnxruntime as ort
# PyTorch 동적 양자화
def quantize_stt_model(model):
"""
동적 양자화: 가중치만 INT8로 변환
- 활성화는 런타임에 양자화
- 정확도 손실 최소화
"""
quantized_model = quantize_dynamic(
model,
{torch.nn.Linear, torch.nn.Conv1d},
dtype=torch.qint8
)
return quantized_model
# ONNX + INT8 양자화 (더 공격적)
def export_and_quantize_onnx(model, output_path):
"""
정적 양자화: 가중치 + 활성화 모두 INT8
- 캘리브레이션 데이터 필요
- 더 작은 모델, 더 빠른 추론
"""
from onnxruntime.quantization import quantize_static, CalibrationDataReader
# ONNX로 내보내기
dummy_input = torch.randn(1, 16000 * 30) # 30초 오디오
torch.onnx.export(
model,
dummy_input,
"model_fp32.onnx",
opset_version=17,
input_names=['audio'],
output_names=['transcription'],
dynamic_axes={'audio': {1: 'audio_length'}}
)
# 양자화
quantize_static(
"model_fp32.onnx",
output_path,
calibration_data_reader=CalibrationDataReader(calibration_data),
quant_format=QuantFormat.QDQ,
per_channel=True,
weight_type=QuantType.QInt8,
activation_type=QuantType.QInt8
)
# 양자화 결과 비교
QUANTIZATION_RESULTS = {
"Whisper-base (FP32)": {
"size": "290MB",
"latency_30s": "1.2s",
"WER": "10.1%"
},
"Whisper-base (INT8)": {
"size": "75MB", # 74% 감소
"latency_30s": "0.4s", # 67% 감소
"WER": "10.3%" # 0.2%p 손실
},
"Whisper-base (INT4)": {
"size": "40MB", # 86% 감소
"latency_30s": "0.3s", # 75% 감소
"WER": "11.2%" # 1.1%p 손실 (트레이드오프)
}
}INT4까지 가면 더 줄일 수 있는데, 여기서부터는 정확도 트레이드오프가 생깁니다. 용도에 따라 선택해야 합니다.
스트리밍 처리
예전에는 전체 오디오를 다 받은 다음에 처리했습니다. 요즘은 오디오가 들어오는 대로 바로바로 처리합니다.
import asyncio
import numpy as np
from dataclasses import dataclass
from typing import Generator, Optional
@dataclass
class StreamingConfig:
chunk_duration_ms: int = 1600 # 1.6초 청크
shift_duration_ms: int = 400 # 0.4초 시프트
sample_rate: int = 16000
lookahead_ms: int = 0 # 실시간은 0
class StreamingSTT:
"""
실시간 스트리밍 STT 구현
핵심 개념:
1. 청크 단위 처리 - 전체 오디오 기다리지 않음
2. 컨텍스트 유지 - 이전 청크 정보 활용
3. 점진적 출력 - 부분 결과 즉시 반환
"""
def __init__(self, model, config: StreamingConfig):
self.model = model
self.config = config
self.buffer = []
self.encoder_cache = None
self.decoder_cache = None
async def process_stream(
self,
audio_stream: Generator[np.ndarray, None, None]
):
"""
오디오 스트림을 실시간으로 처리
"""
chunk_samples = int(
self.config.chunk_duration_ms * self.config.sample_rate / 1000
)
shift_samples = int(
self.config.shift_duration_ms * self.config.sample_rate / 1000
)
accumulated = np.array([], dtype=np.float32)
async for audio_chunk in audio_stream:
accumulated = np.concatenate([accumulated, audio_chunk])
# 청크 크기 도달하면 처리
while len(accumulated) >= chunk_samples:
chunk = accumulated[:chunk_samples]
# 모델 추론 (캐시 활용)
result, self.encoder_cache, self.decoder_cache = \
await self.model.transcribe_chunk(
chunk,
encoder_cache=self.encoder_cache,
decoder_cache=self.decoder_cache
)
# 부분 결과 반환
yield result
# 시프트만큼 이동
accumulated = accumulated[shift_samples:]
# 남은 오디오 처리
if len(accumulated) > 0:
result, _, _ = await self.model.transcribe_chunk(
accumulated,
encoder_cache=self.encoder_cache,
decoder_cache=self.decoder_cache,
is_final=True
)
yield result
# 지연시간 측정
class LatencyProfiler:
"""
스트리밍 STT 지연시간 측정
지연시간 구성요소:
1. 청크 수집 시간 (chunk_duration)
2. 추론 시간 (inference_time)
3. 버퍼링 시간 (buffering)
"""
def measure_latency(self, audio_path: str) -> dict:
import time
streaming_stt = StreamingSTT(model, StreamingConfig())
latencies = []
audio = load_audio(audio_path)
chunk_size = 1600 * 16 # 1.6초
for i in range(0, len(audio), chunk_size):
chunk = audio[i:i+chunk_size]
start = time.perf_counter()
result = streaming_stt.process_chunk(chunk)
elapsed = time.perf_counter() - start
latencies.append(elapsed * 1000) # ms
return {
"mean_latency_ms": np.mean(latencies),
"p50_latency_ms": np.percentile(latencies, 50),
"p90_latency_ms": np.percentile(latencies, 90),
"p99_latency_ms": np.percentile(latencies, 99)
}
# 실제 측정 결과
STREAMING_LATENCY = {
"FastConformer (A100)": {
"p50": "28ms",
"p90": "35ms",
"p99": "52ms"
},
"Whisper-base (A100)": {
"p50": "85ms",
"p90": "120ms",
"p99": "180ms"
}
}FastEmit 같은 기술로 지연시간을 더 줄일 수 있습니다. 90th percentile 기준으로 210ms에서 30ms까지 줄인 사례도 있습니다.
아직 어려운 것들
STT_CHALLENGES_2024 = {
"domain_adaptation": {
"problem": "의료, 법률 같은 전문 분야는 여전히 어려움",
"reason": "전문 용어가 일반 학습 데이터에 부족",
"solution_attempts": [
"도메인 특화 파인튜닝",
"전문 용어 사전 통합",
"Contextual biasing"
],
"current_gap": "일반 도메인 WER 3% vs 전문 도메인 WER 8-15%"
},
"code_switching": {
"problem": "한국어와 영어 섞어서 말하면 잘 못 알아듣는다",
"example": "'이 function을 call하면...' → 인식 실패",
"reason": "언어별로 별도 토크나이저 사용",
"solution_attempts": [
"다국어 통합 모델",
"언어 감지 + 분기 처리"
]
},
"noise_robustness": {
"problem": "시끄러운 환경에서 정확도 급락",
"scenarios": [
"배경 음악",
"여러 화자 동시 발화",
"저품질 마이크"
],
"clean_wer": "3%",
"noisy_wer": "12-25%"
},
"diarization": {
"problem": "누가 말했는지 구분",
"difficulty": "오버랩 구간, 유사한 목소리",
"current_accuracy": "DER 8-15%"
}
}2025년 예측
PREDICTIONS_2025 = {
"edge_adoption": {
"2024": "40% of STT workloads",
"2025": "60-70% (predicted)",
"driver": "프라이버시 규제 강화, 디바이스 성능 향상"
},
"latency": {
"2024": "50-100ms typical",
"2025": "<50ms standard, <20ms achievable",
"enabler": "더 효율적인 모델, 전용 하드웨어"
},
"accuracy": {
"2024": "WER 3-5% (clean speech)",
"2025": "WER <2% (approaching human level)",
"human_level": "WER ~1.5% (전문 타이피스트 수준)"
},
"multimodal": {
"current": "오디오만 처리",
"2025": "오디오 + 비디오 (립리딩) 결합",
"benefit": "노이즈 환경 정확도 유지"
}
}개인적으로는 멀티모달이 기대됩니다. 립리딩과 결합하면 노이즈 환경에서도 정확도를 유지할 수 있을 것입니다.
# 멀티모달 STT 개념
class MultimodalSTT:
"""
Audio + Visual 결합 STT
이점:
- 노이즈 환경에서 강건함
- 동음이의어 구분 향상
- 화자 구분 용이
"""
def __init__(self, audio_encoder, visual_encoder, fusion_model):
self.audio_encoder = audio_encoder
self.visual_encoder = visual_encoder
self.fusion = fusion_model
def transcribe(self, audio, video_frames):
# 오디오 인코딩
audio_features = self.audio_encoder(audio)
# 비주얼 인코딩 (입술 움직임)
visual_features = self.visual_encoder(video_frames)
# 멀티모달 퓨전
fused_features = self.fusion(audio_features, visual_features)
# 디코딩
transcription = self.decoder(fused_features)
return transcription
# 기대 효과
MULTIMODAL_BENEFITS = {
"clean_audio": {"audio_only": "WER 3%", "multimodal": "WER 2.5%"},
"noisy_audio_snr_5db": {"audio_only": "WER 15%", "multimodal": "WER 6%"},
"noisy_audio_snr_0db": {"audio_only": "WER 35%", "multimodal": "WER 12%"}
}참고 자료
- WhisperKit - 온디바이스 Whisper
- FastConformer Paper - NVIDIA
- NeMo Toolkit - NVIDIA ASR 프레임워크
- Whisper - OpenAI
- FastEmit Paper - 저지연 스트리밍
- Audio-Visual Speech Recognition - 멀티모달 연구
Comments