Python 음성 인식 삽질기
Python 음성 인식 삽질기
영화 “Her” 같은 음성 어시스턴트를 만들어보고 싶었다. Verbi라는 오픈소스 프로젝트를 fork해서 커스터마이징했다.
음성 인식 → LLM 응답 → TTS 출력. 간단해 보였는데, 삽질이 많았다.
기본 구조
# 1. 음성 인식 (Whisper)
audio = record_audio()
text = transcribe(audio)
# 2. LLM 응답 (GPT-4)
response = generate_response(text)
# 3. TTS (Deepgram)
audio = text_to_speech(response)
play_audio(audio)
문제 1: Python 3.13 호환성
프로젝트를 실행하자마자 에러:
ModuleNotFoundError: No module named 'audioop'
SpeechRecognition 라이브러리가 audioop 모듈을 사용하는데, Python 3.13에서 deprecated 됐다.
해결: 라이브러리 업그레이드
pip install --upgrade SpeechRecognition
최신 버전(3.10.1+)에서 audioop 대신 순수 Python 구현을 사용한다.
# requirements.txt
SpeechRecognition>=3.10.1
문제 2: 마이크 녹음 AssertionError
녹음 중 갑자기 AssertionError:
AssertionError: Audio source must be entered before recording
원인
SpeechRecognition의 context manager 사용이 잘못됐다.
# 잘못된 코드
source = sr.Microphone()
recognizer.listen(source) # AssertionError!
해결: with 문 사용
# 올바른 코드
with sr.Microphone() as source:
recognizer.adjust_for_ambient_noise(source, duration=0.5)
audio = recognizer.listen(source)
Microphone은 반드시 context manager로 사용해야 한다. __enter__에서 오디오 스트림을 열기 때문.
문제 3: 마이크 감도
조용한 환경에서는 잘 되는데, 약간의 소음만 있어도 인식이 이상해졌다.
원인
기본 에너지 임계값이 너무 낮았다.
해결: 환경 적응형 감도
def setup_microphone(recognizer, source):
# 주변 소음 측정 (1초)
recognizer.adjust_for_ambient_noise(source, duration=1.0)
# 에너지 임계값 상향 (기본값의 1.5배)
recognizer.energy_threshold *= 1.5
# 동적 임계값 비활성화 (안정성)
recognizer.dynamic_energy_threshold = False
print(f"Energy threshold: {recognizer.energy_threshold}")
adjust_for_ambient_noise로 현재 환경의 소음 레벨을 측정하고, 임계값을 약간 높여서 잡음을 필터링.
문제 4: LLM 기반 이름 추출
온보딩에서 사용자 이름을 물어본다:
“안녕하세요! 이름이 뭐예요?”
사용자가 “저는 홍길동이에요”라고 하면, “홍길동”만 추출해야 한다.
해결: LLM으로 이름 추출
def extract_name(user_response: str) -> str:
prompt = f"""
다음 문장에서 사람 이름만 추출하세요.
이름만 출력하고, 다른 설명은 하지 마세요.
문장: "{user_response}"
이름:
"""
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}],
max_tokens=20,
temperature=0
)
return response.choices[0].message.content.strip()
결과:
- “저는 홍길동이에요” → “홍길동”
- “김철수라고 합니다” → “김철수”
- “My name is John” → “John”
문제 5: 온보딩 후 불필요한 대기
온보딩이 끝나면 바로 대화 모드로 넘어가야 하는데, 이상하게 음성 입력을 기다리고 있었다.
원인
온보딩 완료 후 listen() 호출이 중복됐다.
해결: 상태 머신 정리
class VoiceAssistant:
def __init__(self):
self.state = "IDLE"
def run(self):
if not self.is_onboarded():
self.state = "ONBOARDING"
self.do_onboarding()
self.state = "LISTENING"
while True:
# 음성 인식
text = self.listen()
# 응답 생성 및 재생
self.state = "RESPONDING"
response = self.generate_response(text)
self.speak(response)
self.state = "LISTENING"
상태를 명확히 관리해서 중복 호출 방지.
최종 구조
class VoiceAssistant:
def __init__(self):
self.recognizer = sr.Recognizer()
self.setup_recognizer()
def setup_recognizer(self):
with sr.Microphone() as source:
self.recognizer.adjust_for_ambient_noise(source, duration=1.0)
self.recognizer.energy_threshold *= 1.5
self.recognizer.dynamic_energy_threshold = False
def listen(self) -> str:
with sr.Microphone() as source:
print("Listening...")
audio = self.recognizer.listen(source, timeout=5)
# Whisper API로 전사
return self.transcribe(audio)
def transcribe(self, audio) -> str:
audio_data = audio.get_wav_data()
response = openai.audio.transcriptions.create(
model="whisper-1",
file=("audio.wav", audio_data),
language="ko"
)
return response.text
def speak(self, text: str):
# Deepgram TTS
audio = self.tts.speak(text)
self.play_audio(audio)
삽질 정리
| 문제 | 해결 |
|---|---|
| Python 3.13 audioop 없음 | SpeechRecognition >= 3.10.1 |
| AssertionError | Microphone with 문 사용 |
| 마이크 감도 | energy_threshold 조정 |
| 이름 추출 | LLM prompt |
| 상태 관리 | 상태 머신 패턴 |
결론
Python 음성 인식은 라이브러리 덕분에 쉬워 보이지만, 실제 환경에서는 삽질이 많다.
특히:
- Python 버전에 따른 호환성 문제
- 마이크 환경에 따른 감도 조정
- 비동기 처리와 상태 관리
“Hey Siri” 하나 만드는 데 Apple이 수천 명을 고용한 이유를 알겠다.