이번 편에서 만들 것
1편에서 ESP32에 귀(마이크)를 달고 Wake Word를 감지하게 만들었습니다. 이번 편에서는 녹음된 음성을 WiFi로 서버에 보내고, AI가 이해한 뒤 다시 음성으로 답변해주는 STT, LLM, TTS 파이프라인을 구축합니다.
전체 흐름도
ESP32-S3 ──WiFi──> FastAPI 서버
(오디오) (오케스트레이터)
┌──────────┐
│ Whisper │ 음성 → 텍스트
│ GPT-4o │ 텍스트 → 응답
│ TTS │ 응답 → 음성
└──────────┘
ESP32-S3 <──PCM── 서버 응답
(스피커 재생)
Edge-Cloud 하이브리드 음성 비서 아키텍처
1. 서버 아키텍처: FastAPI 오케스트레이터
서버는 하나의 FastAPI 앱이 모든 AI 처리를 오케스트레이션합니다. ESP32는 이 서버 하나만 알면 됩니다.
# server.py — FastAPI 오케스트레이터
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import StreamingResponse
import openai
import tempfile
import io
app = FastAPI()
# 설정
STT_MODEL = "whisper-large-v3-turbo"
LLM_MODEL = "gpt-4o"
TTS_MODEL = "tts-1"
TTS_VOICE = "nova"
@app.post("/api/voice")
async def process_voice(audio: UploadFile = File(...)):
# 1. STT: 음성을 텍스트로
text = await speech_to_text(audio)
print(f"STT: {text}")
# 2. LLM: 텍스트로 응답 생성
reply = await chat_with_llm(text)
print(f"LLM: {reply}")
# 3. TTS: 응답을 음성으로
audio_stream = await text_to_speech(reply)
return StreamingResponse(
audio_stream,
media_type="audio/wav",
headers={"X-Text-Response": reply}
)
async def speech_to_text(audio: UploadFile) -> str:
with tempfile.NamedTemporaryFile(suffix=".wav",
delete=False) as f:
f.write(await audio.read())
temp_path = f.name
client = openai.OpenAI()
with open(temp_path, "rb") as f:
transcript = client.audio.transcriptions.create(
model=STT_MODEL, file=f,
response_format="text", language="ko"
)
return transcript.strip()
async def chat_with_llm(text: str) -> str:
client = openai.OpenAI()
response = client.chat.completions.create(
model=LLM_MODEL,
messages=[
{"role": "system", "content":
"You are JARVIS. Korean, concise, 3 sentences max."},
{"role": "user", "content": text}
]
)
return response.choices[0].message.content
async def text_to_speech(text: str):
client = openai.OpenAI()
response = client.audio.speech.create(
model=TTS_MODEL, voice=TTS_VOICE,
input=text, response_format="wav"
)
return io.BytesIO(response.content)
2. STT: 음성을 텍스트로 변환
STT(Speech-to-Text)는 음성 인식의 핵심입니다. 세 가지 옵션을 비교해봅시다.
| 서비스 | 지연 | 비용 | 한국어 | 비고 |
|---|---|---|---|---|
| OpenAI Whisper API | 1~2초 | $0.006/분 | 우수 | 가장 간단한 구현 |
| Google Speech-to-Text | 0.5~1초 | $0.006/분 | 우수 | 스트리밍 지원 |
| 로컬 Whisper | 2~5초 | 무료 | 우수 | GPU 필요, 개인정보 보호 |
Whisper API 사용 (추천)
from openai import OpenAI
client = OpenAI()
with open("recording.wav", "rb") as f:
transcript = client.audio.transcriptions.create(
model="whisper-large-v3-turbo",
file=f,
language="ko" # 한국어 명시로 인식률 향상
)
print(transcript.text) # "오늘 날씨 어때?"
로컬 Whisper (비용 없이)
# pip install faster-whisper
from faster_whisper import WhisperModel
model = WhisperModel("large-v3-turbo",
device="cuda",
compute_type="float16")
segments, _ = model.transcribe("recording.wav",
language="ko")
text = " ".join(s.text for s in segments)
3. LLM: 지능적인 응답 생성
| 모델 | 응답 속도 | 한국어 | 비용 |
|---|---|---|---|
| GPT-4o | ~1초 | 최상 | $2.5/1M토큰 |
| GPT-4o-mini | ~0.5초 | 우수 | $0.15/1M토큰 |
| Claude Sonnet 4 | ~1초 | 최상 | $3/1M토큰 |
| 로컬 Llama 3.1 | 2~10초 | 양호 | 무료 (GPU 필요) |
시스템 프롬프트: 자비스의 성격 정의
SYSTEM_PROMPT = """
너는 JARVIS(자비스)다. 아이언맨의 AI 비서처럼 동작한다.
규칙:
- 한국어로 대답한다
- 간결하게 대답한다 (3문장 이내)
- 필요시 유머를 섞는다
- 모르는 건 솔직하게 모른다고 한다
- 시간, 날씨, 일정 등 일상적인 질문에 답한다
- 스마트홈 기기 제어 명령을 이해한다
"""
4. TTS: 음성 합성으로 답변
| 서비스 | 한국어 | 지연 | 비용 |
|---|---|---|---|
| OpenAI TTS | 자연스러움 | 0.5~1초 | $15/1M문자 |
| Google Cloud TTS | 우수 | 0.3~0.5초 | $4/1M문자 |
| ElevenLabs | 가장 자연스러움 | 1~2초 | $5/월 |
| 로컬 Piper TTS | 기계음 | 0.5초 | 무료 |
추천: OpenAI TTS의 nova 또는 alloy 보이스. 한국어 발음이 가장 자연스럽고, API 호출 한 번으로 WAV 파일을 받을 수 있습니다.
5. ESP32 펌웨어: 서버와 통신
// ESP32에서 서버로 오디오 전송 + 응답 재생
#include <esp_http_client.h>
#define SERVER_URL "http://192.168.1.100:8080/api/voice"
void send_audio_to_server(uint8_t *audio, size_t len) {
esp_http_client_config_t config = {
.url = SERVER_URL,
.method = HTTP_METHOD_POST,
.timeout_ms = 30000,
};
esp_http_client_handle_t client =
esp_http_client_init(&config);
esp_http_client_set_header(client, "Content-Type",
"multipart/form-data; boundary=----ESP32");
esp_http_client_set_post_field(client,
(char *)audio, len);
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
int status =
esp_http_client_get_status_code(client);
printf("Response: %d\n", status);
// 응답(TTS 오디오)을 I2S로 스트리밍 재생
uint8_t buf[1024];
int read_len, written;
while ((read_len =
esp_http_client_read(client,
(char *)buf, 1024)) > 0) {
i2s_write(I2S_NUM_0, buf, read_len,
&written, portMAX_DELAY);
}
}
esp_http_client_cleanup(client);
}
6. 전체 동작 시퀀스
| 시간 | 주체 | 동작 |
|---|---|---|
| 0.0s | 사용자 | "Hey Jarvis!" |
| 0.1s | ESP32 | WakeNet 감지, LED 파란색, 마이크 활성화 |
| 0.2s | 사용자 | "오늘 날씨 어때?" |
| 2.5s | ESP32 | VAD 침묵 감지, 녹음 종료 |
| 2.6s | ESP32 | WiFi로 오디오 POST 전송 |
| 3.5s | 서버 | Whisper STT: "오늘 날씨 어때?" |
| 4.0s | 서버 | GPT-4o: "현재 서울은 맑고 22도입니다." |
| 4.8s | 서버 | TTS 음성 WAV 생성 |
| 4.9s | ESP32 | I2S로 스피커 재생 시작 🔊 |
| ~6s | ESP32 | 재생 완료, 대기 모드 복귀 (LED 초록색) |
7. 지연 최적화
음성 비서에서 지연(Latency)은 사용자 경험의 핵심입니다. 목표는 3초 이내입니다.
| 단계 | 평균 소요 | 최적화 |
|---|---|---|
| 음성 녹음 | ~2초 | VAD로 말 끝 빠르게 감지 |
| WiFi 전송 | ~0.1초 | Opus 코덱 압축 |
| Whisper STT | ~1초 | large-v3-turbo 사용 |
| GPT-4o 응답 | ~0.5초 | max_tokens 제한 |
| TTS 생성 | ~0.8초 | 짧은 응답 텍스트 |
| 수신 + 재생 | ~0.2초 | 스트리밍 재생 |
| 총합 | ~4.6초 | 최적화 시 ~2.5초 |
핵심 최적화 팁
- 스트리밍 TTS: 텍스트 생성 즉시 TTS 시작, 첫 음절 지연 단축
- Opus 코덱: WAV 대신 Opus 압축으로 전송 크기 10분의 1
- 로컬 STT: GPU 서버에 faster-whisper 구축으로 API 호출 제거
이번 편 요약
- FastAPI 서버에 /api/voice 엔드포인트 구축
- Whisper API로 한국어 음성 인식 (STT)
- GPT-4o로 지능적인 응답 생성 (LLM)
- OpenAI TTS로 한국어 음성 합성
- ESP32에서 WiFi HTTP POST로 오디오 전송 + 응답 재생
- 전체 지연 ~2.5~3초 최적화
다음 3편(최종편)에서는 3.5인치 터치 디스플레이에 JARVIS 스타일 UI를 구현합니다. 말하는 동안의 애니메이션, 음성 파형, 날씨 위젯 등을 LVGL로 만들어봅니다.
참고 링크
- KALO ESP32 Voice Chat — 완성된 레퍼런스
- OpenAI Whisper API
- OpenAI TTS API
- faster-whisper — 로컬 STT (GPU 가속)