Whisper.cpp와 FSM을 활용한 스페인어 발음 인식 텔레그램 봇 구축
스페인어 연습을 돕는 텔레그램 봇은 클라우드 API에 의존하지 않고 로컬에서 동작하는 whisper.cpp 엔진을 사용해 음성 인식 기능을 구현합니다. 이 시스템은 참조 문장과의 발음 비교를 통해 정확도를 평가하고, 외부 LLM을 활용한 대화형 상호작용을 지원하며, 자원 제한이 있는 VPS 환경에서도 효율적으로 작동합니다. 비동기적 aiogram 3.x와 whisper.cpp 바이너리를 결합함으로써 여러 요청을 동시에 처리할 수 있습니다.
비차단적인 Whisper.cpp 통합
표준 subprocess.run()은 aiogram 이벤트 루프를 차단합니다. 해결책은 무엇일까요? 바로 asyncio.create_subprocess_exec을 사용해 비동기 실행을 하고, asyncio.to_thread로 무거운 pydub 작업을 별도 스레드로 분리하는 것입니다.
import asyncio
from pydub import AudioSegment
import subprocess
import os
async def recognize_voice_async(file_path: str) -> str:
wav_path = "temp_voice.wav"
txt_path = wav_path + ".txt"
try:
# 1. 오디오를 Whisper 호환 형식(16kHz, 모노, WAV)으로 변환
# pydub 처리를 스레드로 이동해 이벤트 루프 차단 방지
audio = AudioSegment.from_file(file_path)
await asyncio.to_thread(
lambda: audio.set_frame_rate(16000)
.set_channels(1)
.set_sample_width(2)
.export(wav_path, format="wav")
)
if not os.path.exists(wav_path):
raise FileNotFoundError("WAV 파일 생성 실패")
# 2. whisper.cpp 비동기 실행
process = await asyncio.create_subprocess_exec(
"/root/whisper.cpp/build/bin/whisper-cli", # 바이너리 경로
"-m", "/root/whisper.cpp/models/ggml-tiny.bin", # 미니 모델
"-f", wav_path,
"--language", "es",
"--output-txt",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
print(f"[ERROR] Whisper 실패: {stderr.decode()}")
return ""
# 3. 결과 읽기
if os.path.exists(txt_path):
with open(txt_path, "r", encoding="utf-8") as f:
text = f.read().strip()
return text
return ""
except Exception as e:
print(f"[CRITICAL ERROR] {e}")
return ""
finally:
# 임시 파일 정리
for f in [wav_path, txt_path, file_path]:
if os.path.exists(f):
os.remove(f)
이 코드는 봇의 멈춤 없이 실시간 음성 인식을 보장합니다. ggml-tiny.bin 모델은 CPU에서 빠른 성능을 위해 선택되었습니다.
발음 정확도 평가 알고리즘
문자열 간 단순 비교는 어미나 관사 오류가 잦은 Whisper의 특성상 신뢰할 수 없습니다. 대신 단어별 위치 기반 점수 계산 알고리즘을 사용합니다.
def calculate_accuracy(expected: str, recognized: str) -> int:
if not recognized:
return 0
expected_words = expected.lower().split()
recognized_words = recognized.lower().split()
if not expected_words:
return 0
# 위치 기반 단어 비교
min_len = min(len(expected_words), len(recognized_words))
matches = sum(1 for i in range(min_len) if expected_words[i] == recognized_words[i])
# 공식: (일치 수 / 기준 길이) * 100
accuracy = int((matches / len(expected_words)) * 100)
return max(0, min(100, accuracy))
예시 결과:
- 완전 일치: 100%
- 마지막 단어 누락: 80–90%
- 전혀 일치 없음: 0–20%
성공 기준은 70%로 설정되어 있으며, 향후 내부 단어 오류 탐지를 위해 레벤슈타인 거리 기법을 도입할 예정입니다.
상태 관리 및 라우팅을 위한 FSM
aiogram.fsm 상태 기계는 두 가지 모드를 분리합니다: 자유 대화 모드와 발음 훈련 모드.
from aiogram.fsm.state import State, StatesGroup
class UserStates(StatesGroup):
waiting_for_ai_dialog = State() # 자유 대화 모드
통합 음성 메시지 핸들러:
@dp.message(F.voice)
async def universal_voice_handler(message: Message, state: FSMContext):
current_state = await state.get_state()
# BRANCH 1: AI 대화
if current_state == "UserStates:waiting_for_ai_dialog":
from handlers.voice_ai import handle_ai_dialog_voice
await handle_ai_dialog_voice(message, bot, state)
return
# BRANCH 2: 발음 훈련(수업)
# ... 참조 문장 가져오기, 인식, 점수 계산 로직
기술 스택 및 데이터 저장소
- STT: whisper.cpp (ggml-tiny.bin, 스페인어)
- TTS: gTTS + pydub
- LLM: 외부 API
- 저장소: JSON 파일 (users.json, lessons.json, phrases.json)
관리자 명령어(/addphrase, /stats)를 통해 콘텐츠를 동적으로 업데이트 가능하며, PostgreSQL과 Docker를 활용한 확장 가능한 배포가 가능합니다.
주요 장점:
- 비동기 whisper.cpp 통합으로 봇 동작 중단 방지
- 단어 수준 비교로 클라우드 의존 없이 정확한 발음 피드백 제공
- FSM으로 모드 확장이 쉽고 핸들러 재작성 필요 없음
- 로컬 처리로 개인정보 보안과 비용 효율성 확보
- tiny.bin 모델은 CPU에서 최소 지연으로 빠르게 작동
— Editorial Team
아직 댓글이 없습니다.