나만의 자비스 만들기 2편 — 귀에서 입까지, STT·LLM·TTS 파이프라인

나만의 자비스 만들기 2편 — 귀에서 입까지, STT·LLM·TTS 파이프라인

ESP32에서 녹음한 음성을 AI 서버로 보내고, 텍스트로 변환해 답변을 다시 음성으로 돌려받는 전체 과정
📅 2026년 5월 24일 ✍️ Bongjoo ESP32 JARVIS STT LLM TTS FastAPI

이번 편에서 만들 것

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 API1~2초$0.006/분우수가장 간단한 구현
Google Speech-to-Text0.5~1초$0.006/분우수스트리밍 지원
로컬 Whisper2~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.12~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.1sESP32WakeNet 감지, LED 파란색, 마이크 활성화
0.2s사용자"오늘 날씨 어때?"
2.5sESP32VAD 침묵 감지, 녹음 종료
2.6sESP32WiFi로 오디오 POST 전송
3.5s서버Whisper STT: "오늘 날씨 어때?"
4.0s서버GPT-4o: "현재 서울은 맑고 22도입니다."
4.8s서버TTS 음성 WAV 생성
4.9sESP32I2S로 스피커 재생 시작 🔊
~6sESP32재생 완료, 대기 모드 복귀 (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초

핵심 최적화 팁

이번 편 요약

다음 3편(최종편)에서는 3.5인치 터치 디스플레이에 JARVIS 스타일 UI를 구현합니다. 말하는 동안의 애니메이션, 음성 파형, 날씨 위젯 등을 LVGL로 만들어봅니다.

참고 링크

← 모든 글 보기