음성 대화 반복 과정 만들기
녹음 → 전사 → 추론 → 발화 주기를 중단 처리와 함께 구현합니다.
음성 대화 반복 과정 만들기은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.
음성 대화 루프
완전한 음성 대화 루프는 마이크 입력과 에이전트 출력을 연속적인 주기로 연결합니다. 녹음 → 전사 → 에이전트 → TTS → 재생 → 반복의 흐름입니다.
이 단원에서는 각 구성 요소와 음성 루프를 텍스트 기반 에이전트와 다르게 만드는 문제를 다룹니다. 여기에는 무음 감지, 인터럽트 처리, 발화 종료 감지가 포함됩니다.
마이크에서 녹음하기
sounddevice를 사용하여 기본 마이크에서 오디오를 캡처합니다. 고정된 시간 동안 녹음하거나 무음이 감지될 때까지 녹음할 수 있습니다. 위스퍼가 요구하는 샘플 속도인 16kHz 모노로 항상 녹음하세요.
pip install sounddevice soundfile numpy로 설치합니다.
import sounddevice as sd
import numpy as np
import tempfile
import soundfile as sf
SAMPLE_RATE = 16000 # 16kHz mono — optimal for Whisper
DURATION = 5 # seconds
def record_audio(duration=DURATION, sample_rate=SAMPLE_RATE):
print(f'Recording for {duration} seconds...')
audio_data = sd.rec(
int(duration * sample_rate),
samplerate=sample_rate,
channels=1,
dtype='float32'
)
sd.wait() # block until recording finishes
print('Recording complete')
return audio_data, sample_rate
def audio_to_file(audio_data, sample_rate, filepath='/tmp/recording.wav'):
sf.write(filepath, audio_data, sample_rate)
return filepath발화 종료를 위한 무음 감지
고정된 시간 동안 녹음하는 방식은 불편합니다. 사용자가 2초 만에 말을 끝냈더라도 기다려야 하기 때문입니다. 무음 감지는 사용자가 일정 시간 동안 조용히 있으면 자동으로 녹음을 중지합니다.
import sounddevice as sd
import numpy as np
from collections import deque
SAMPLE_RATE = 16000
CHUNK_SIZE = 1024 # samples per chunk
SILENCE_THRESHOLD = 0.02 # RMS volume below this = silence
SILENCE_DURATION = 1.5 # seconds of silence to end recording
MAX_DURATION = 30 # max recording length in seconds
def record_until_silence():
chunks = []
silent_chunks = 0
max_silent = int(SILENCE_DURATION * SAMPLE_RATE / CHUNK_SIZE)
max_chunks = int(MAX_DURATION * SAMPLE_RATE / CHUNK_SIZE)
print('Listening... (speak now)')
with sd.InputStream(samplerate=SAMPLE_RATE, channels=1,
blocksize=CHUNK_SIZE, dtype='float32') as stream:
while True:
chunk, _ = stream.read(CHUNK_SIZE)
chunks.append(chunk.copy())
rms = np.sqrt(np.mean(chunk**2))
if rms < SILENCE_THRESHOLD:
silent_chunks += 1
else:
silent_chunks = 0 # speech detected — reset counter
if silent_chunks >= max_silent and len(chunks) > max_silent:
break
if len(chunks) >= max_chunks:
break
audio = np.concatenate(chunks, axis=0)
return audio, SAMPLE_RATE전체 음성 루프
모든 구성 요소를 하나의 연속적인 루프로 연결합니다. 에이전트가 응답한 후 즉시 다시 듣기 시작하면 자연스러운 양방향 대화가 만들어집니다.
import openai
import os
client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
conversation_history = []
def voice_conversation_loop():
print('Voice agent started. Say something (Ctrl+C to exit).')
while True:
# 1. Record user
audio_data, sample_rate = record_until_silence()
audio_path = audio_to_file(audio_data, sample_rate)
# 2. Transcribe
user_text = transcribe_file(audio_path)
print(f'You: {user_text}')
if not user_text.strip():
continue # empty — listen again
# 3. Run agent
conversation_history.append({'role': 'user', 'content': user_text})
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'system', 'content': 'You are a helpful voice assistant. Keep responses concise.'}] + conversation_history[-10:]
)
agent_text = response.choices[0].message.content
conversation_history.append({'role': 'assistant', 'content': agent_text})
print(f'Agent: {agent_text}')
# 4. TTS and play
say(agent_text) # speaks sentence by sentence호출어 감지 개념
항상 듣는 방식은 지속적인 위스퍼 호출로 인해 비용이 많이 들고 사생활을 침해할 수 있습니다. 호출어 감지는 가벼운 로컬 모델을 실행하며, 특정 문구(예: 'Hey Agent')가 들릴 때만 전체 처리를 시작합니다.
# Wake word detection with pvporcupine (Picovoice)
# pip install pvporcupine sounddevice
import pvporcupine
import sounddevice as sd
import numpy as np
import os
def listen_for_wake_word(wake_word='computer'):
porcupine = pvporcupine.create(
access_key=os.getenv('PICOVOICE_KEY'),
keywords=[wake_word] # built-in: computer, hey barista, hey google, jarvis...
)
print(f'Listening for wake word: "{wake_word}"...')
with sd.InputStream(
samplerate=porcupine.sample_rate,
channels=1,
dtype='int16',
blocksize=porcupine.frame_length
) as stream:
while True:
frame, _ = stream.read(porcupine.frame_length)
pcm = frame.flatten().astype('int16')
result = porcupine.process(pcm)
if result >= 0:
print(f'Wake word detected!')
porcupine.delete()
return True인터럽트 처리
사용자는 에이전트가 말하는 중에도 에이전트를 중단할 수 있어야 합니다. 별도의 스레드에서 재생을 실행하고 마이크 음량을 모니터링하여 인터럽트 처리를 구현하세요. 사용자가 말하기 시작하면 즉시 재생을 중지합니다.
import threading
import sounddevice as sd
import numpy as np
interrupt_event = threading.Event()
def monitor_for_interrupt(threshold=0.03):
def audio_callback(indata, frames, time_info, status):
rms = np.sqrt(np.mean(indata**2))
if rms > threshold:
print('Interrupt detected!')
interrupt_event.set()
with sd.InputStream(callback=audio_callback, channels=1,
samplerate=16000, blocksize=1024):
while not interrupt_event.is_set():
sd.sleep(50)
def speak_with_interrupt(text, voice='nova'):
interrupt_event.clear()
# Start interrupt monitor in background
monitor_thread = threading.Thread(target=monitor_for_interrupt, daemon=True)
monitor_thread.start()
# Play audio sentence by sentence
for sentence in split_into_sentences(text):
if interrupt_event.is_set():
print('Playback interrupted')
break
audio_path = cached_tts(sentence, voice)
play_audio_file(audio_path)잡음 필터링
마이크 오디오에는 팬 소리, 키보드 소리, 실내 잔향과 같은 배경 잡음이 자주 포함됩니다. 간단한 노이즈 게이트를 적용하여 임계값보다 작은 오디오를 억제하고, 더 정확한 감지를 위해 음성 활동 감지기(VAD)를 선택적으로 사용할 수 있습니다.
import numpy as np
NOISE_GATE_THRESHOLD = 0.015 # RMS threshold
def apply_noise_gate(audio_data, threshold=NOISE_GATE_THRESHOLD):
rms = np.sqrt(np.mean(audio_data**2))
if rms < threshold:
return np.zeros_like(audio_data) # silence below threshold
return audio_data
def has_speech_content(audio_data, threshold=0.02):
rms_values = [
np.sqrt(np.mean(audio_data[i:i+1600]**2))
for i in range(0, len(audio_data), 1600)
]
speech_frames = sum(1 for r in rms_values if r > threshold)
speech_ratio = speech_frames / max(len(rms_values), 1)
return speech_ratio > 0.1 # at least 10% of frames have speech
# Use in loop before transcribing
audio, sr = record_until_silence()
if has_speech_content(audio):
text = transcribe_file(audio_to_file(audio, sr))
else:
print('No speech detected — listening again')대화 루프를 위한 상태 머신
견고한 음성 루프는 상태 머신을 사용하여 에이전트가 어떤 작업을 수행 중인지 항상 추적하고, 재생과 듣기 사이의 경쟁 조건을 방지합니다.
from enum import Enum
class AgentState(Enum):
IDLE = 'idle'
LISTENING = 'listening'
TRANSCRIBING = 'transcribing'
THINKING = 'thinking'
SPEAKING = 'speaking'
current_state = AgentState.IDLE
def set_state(new_state):
global current_state
print(f'State: {current_state.value} -> {new_state.value}')
current_state = new_state
def voice_loop_with_state():
while True:
set_state(AgentState.LISTENING)
audio, sr = record_until_silence()
if not has_speech_content(audio):
continue
set_state(AgentState.TRANSCRIBING)
text = transcribe_file(audio_to_file(audio, sr))
if not text.strip():
continue
print(f'You: {text}')
set_state(AgentState.THINKING)
agent_response = run_agent(text)
print(f'Agent: {agent_response}')
set_state(AgentState.SPEAKING)
speak_with_interrupt(agent_response)대화 맥락 관리
음성 에이전트는 대화 기록을 유지하므로 “그 부분에 대해 더 말씀해 주세요” 또는 “가격이 얼마라고 말씀하셨죠?”와 같은 후속 질문에 답할 수 있습니다. 기록은 메모리에 보관하되, 맥락이 넘치지 않도록 마지막 N번의 대화 차례만 남기세요.
import openai
import os
client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
MAX_HISTORY_TURNS = 10
SYSTEM_PROMPT = 'You are a concise voice assistant. Keep responses under 3 sentences unless asked for detail.'
class VoiceAgent:
def __init__(self):
self.history = []
def respond(self, user_text):
self.history.append({'role': 'user', 'content': user_text})
# Trim history to last N turns
recent = self.history[-(MAX_HISTORY_TURNS * 2):]
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'system', 'content': SYSTEM_PROMPT}] + recent
)
agent_text = response.choices[0].message.content
self.history.append({'role': 'assistant', 'content': agent_text})
return agent_text
def reset(self):
self.history = []
print('Conversation history cleared')정상 종료 및 정리
Ctrl+C와 기타 종료 신호를 정상적으로 처리하세요. 오디오 스트림을 중지하고, 임시 파일을 정리하며, 종료하기 전에 작별 인사를 합니다.
import signal
import sys
import glob
import os
TMP_DIR = '/tmp'
agent = None
def cleanup_temp_files():
for f in glob.glob(os.path.join(TMP_DIR, 'recording_*.wav')):
os.unlink(f)
print('Temp files cleaned up')
def graceful_shutdown(signum, frame):
print('\nShutting down voice agent...')
if agent:
say('Goodbye! Have a great day.')
cleanup_temp_files()
sys.exit(0)
# Register signal handler
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)
# Main entry point
agent = VoiceAgent()
print('Voice agent ready. Press Ctrl+C to exit.')
voice_loop_with_state()대화 기록 남기기
대화의 지속적인 기록을 유지하세요. 타임스탬프, 전사한 텍스트, 에이전트 응답, 발생한 오류를 기록합니다. 이는 음성 에이전트 문제를 디버깅하고 대화 검토 기능을 구축하는 데 필수적입니다.
import json
import os
from datetime import datetime
LOG_DIR = '/tmp/voice_agent_logs'
os.makedirs(LOG_DIR, exist_ok=True)
conversation_log = []
def log_turn(speaker, text, latency_ms=None, error=None):
entry = {
'timestamp': datetime.now().isoformat(),
'speaker': speaker, # 'user' or 'agent'
'text': text,
'latency_ms': latency_ms
}
if error:
entry['error'] = error
conversation_log.append(entry)
def save_conversation_log(session_id=None):
if not session_id:
session_id = datetime.now().strftime('%Y%m%d_%H%M%S')
log_path = os.path.join(LOG_DIR, f'session_{session_id}.json')
with open(log_path, 'w') as f:
json.dump({
'session_id': session_id,
'turns': len(conversation_log),
'log': conversation_log
}, f, indent=2)
print(f'Log saved: {log_path}')
return log_path
# Usage in voice loop
log_turn('user', transcribed_text, latency_ms=420)
log_turn('agent', agent_response, latency_ms=850)지식 확인
음성 대화 루프에서 무음 감지의 목적은 무엇인가요?
복습: 음성 대화 루프 구축
음성 대화 루프는 다음과 같이 구성됩니다. 무음 감지를 사용해 마이크에서 녹음 → 위스퍼로 전사 → 대화 기록과 함께 에이전트 실행 → 문장별 TTS 응답 → 인터럽트 모니터링과 함께 재생 → 반복
핵심 구성 요소는 경쟁 조건을 방지하는 상태 머신, 상시 청취를 위한 호출어 감지, 오디오 품질을 위한 노이즈 게이트, 맥락 관리를 위한 대화 기록 정리입니다. Ctrl+C는 정리 작업과 작별 메시지를 포함하여 정상적으로 처리하세요.
자주 묻는 질문
“음성 대화 반복 과정 만들기” 강의는 무료인가요?
네 — “음성 대화 반복 과정 만들기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.
“음성 대화 반복 과정 만들기”에서 뭘 배우나요?
녹음 → 전사 → 추론 → 발화 주기를 중단 처리와 함께 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Agents을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“음성 대화 반복 과정 만들기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.