Java AI 어시스턴트에 음성 기능 추가하기 — Whisper, TTS, 그리고 음성 대화 루프
요약
Java 기반 AI 플랫폼 Jarvis에 음성 기능을 통합하는 과정을 다룹니다. Whisper를 활용한 음성 인식(STT)과 OS 네이티브 엔진을 이용한 음성 합성(TTS)을 기존 채팅 파이프라인에 래퍼 형태로 결합하는 아키텍처를 설명합니다.
핵심 포인트
- Ollama는 Whisper를 지원하지 않으므로 Groq API나 whisper.cpp를 대안으로 사용
- 음성 기능을 기존 채팅 파이프라인을 감싸는 래퍼 구조로 설계하여 확장성 확보
- 블로킹 I/O 처리를 위해 WebFlux의 boundedElastic() 스케줄러 활용
- 추가 비용 없는 TTS 구현을 위해 OS 네이티브 음성 엔진 활용
우리가 어떻게 Jarvis에게 듣고 말하는 능력을 부여했는지 — Jarvis AI 플랫폼의 5단계 (Phase 5)
지난 단계 복습
4단계 (Phase 4) 이후, Jarvis는 실제 도구들을 사용하여 질문에 답할 수 있었습니다.
사용자: 카트만두의 날씨는 어때?
Jarvis: [WeatherTool 호출] 현재 22°C이며 맑습니다.
...
하지만 모든 상호작용에는 타이핑이 필요했습니다.
5단계(Phase 5)는 그것을 변화시켰습니다.
목표
5단계 이전:
사용자가 타이핑함 → Jarvis가 타이핑으로 답변함
...
설명하기는 간단합니다.
하지만 올바르게 구축하는 것은 놀라울 정도로 미묘한 차이가 있습니다.
첫 번째 놀라움 — Ollama는 Whisper를 지원하지 않음
원래 계획은 Ollama를 통해 Whisper를 로컬에서 실행하는 것이었습니다.
ollama pull whisper
## Error:
...
Ollama는 언어 모델(Language Models)에는 탁월합니다.
하지만 오디오 전사 모델(Audio Transcription Models)은 지원하지 않습니다.
이로 인해 재고가 필요해졌습니다.
해결책 — 두 가지 모드
우리는 두 가지 백엔드를 지원하도록 WhisperTranscriptionService를 설계했습니다.
모드 1 — Groq API (클라우드)
Groq는 OpenAI 호환 API를 통해 Whisper large-v3-turbo를 제공합니다.
무료 티어는 신용카드 등록 없이 일일 6,000회 요청을 제공합니다.
.env 파일에 GROQ_API_KEY 설정
↓
...
모드 2 — 로컬 whisper.cpp
완전한 로컬 전사를 원하는 사용자를 위한 방법입니다:
git clone https://github.com/ggerganov/whisper.cpp
cd whisper.cpp
...
두 구현체 모두 동일한 OpenAI 호환 멀티파트(Multipart) API를 노출합니다.
이들 사이의 전환은 단 하나의 설정 플래그로 이루어집니다.
아키텍처 — 핵심 결정
5단계의 가장 중요한 아키텍처 결정은 다음과 같습니다:
음성은 기존 채팅 파이프라인(Chat Pipeline)을 감싸는 래퍼(Wrapper)일 뿐입니다.
AiOrchestrator는 변경되지 않습니다.
❌ 잘못된 방식
Voice Pipeline
...
1~4단계에서 구축된 모든 기능은 자동으로 계속 작동합니다.
WhisperTranscriptionService
@Service
public class WhisperTranscriptionService {
...
두 가지 설계 선택 사항을 강조할 가치가 있습니다.
Schedulers.boundedElastic()
Groq 또는 whisper.cpp를 호출하는 것은 블로킹 I/O (Blocking I/O)입니다.
이를 WebFlux 이벤트 루프(Event Loop)에서 실행하면 모든 요청이 차단될 것입니다.
boundedElastic()은 리액티브 이벤트 루프 (Reactive Event Loop)를 자유롭게 유지합니다.
isLocalMode
로컬 whisper.cpp는 API 키가 필요하지 않습니다.
하나의 불리언 (Boolean) 값만으로 비즈니스 로직을 변경하지 않고도 백엔드를 전환할 수 있습니다.
Text-to-Speech (TTS) — 크로스 플랫폼의 과제
또 다른 의존성 (Dependency)을 추가하는 대신, 운영체제 (OS) 네이티브 음성 엔진을 선택했습니다.
이유는 무엇일까요?
- 추가 라이브러리 없음
- API 키 불필요
- 즉시 작동
- 충분히 좋은 품질
- 완전한 오프라인 작동
플랫폼 지원:
Windows → PowerShell + System.Speech.Synthesis
macOS → say
...
서비스는 시작 시점에 플랫폼을 한 번 감지합니다.
private static final boolean IS_WINDOWS = OS.contains("win");
private static final boolean IS_MAC = OS.contains("mac");
private static final boolean IS_LINUX =
...
음성 설정은 전적으로 환경 (Environment)에 의해 구동됩니다.
## Windows
JARVIS_VOICE_NAME=Microsoft Zira Desktop
...
DST 인식 — 놀랍도록 까다로운 버그
코드 리뷰를 통해 이 미묘한 문제를 발견했습니다.
// ❌ 잘못된 방식
TimeZone.getTimeZone(zoneId)
.getDisplayName(false,
...
이 방식은 항상 **표준시 (Standard Time)**를 보고합니다.
올바른 구현은 현재의 일광 절약 시간제 (DST) 상태를 도출하는 것입니다.
boolean isDst =
TimeZone.getTimeZone(zoneId)
.inDaylightTime(Date.from(now.toInstant()));
...
이 수정 사항이 없다면, DST 지역의 사용자들은 일 년의 절반 동안 잘못된 시간대 이름을 보게 될 것입니다.
문장 버퍼링 (Sentence Buffering) 문제
LLM은 토큰 (Token)을 스트리밍합니다.
"The"
"weather"
...
개별 토큰을 하나씩 소리 내어 읽는 것은 매우 어색하게 들립니다.
해결책은 문장 버퍼링 (Sentence Buffering)이었습니다.
private void startTtsPipeline(Flux<String> tokenStream) {
StringBuilder buffer = new StringBuilder();
...
세 가지 구현 세부 사항이 중요합니다.
concatMap()
문장은 반드시 순차적으로 재생되어야 합니다.
flatMap()을 사용하면 여러 오디오 스트림이 겹치게 됩니다.
MAX_BUFFER_TOKENS
일부 AI 응답에는 문장 부호가 포함되지 않을 수 있습니다.
50단어 이후에는 자동으로 버퍼를 비웁니다 (Flush).
백그라운드 실행 (Background execution)
음성 생성은 boundedElastic()에서 수행됩니다.
브라우저는 즉시 스트리밍되는 토큰을 계속해서 받습니다.
두 개의 파이프라인 아키텍처 (The Two-Pipeline Architecture)
첫 번째 구현 방식은 스트리밍을 차단(Block)했습니다.
❌ 잘못된 방식 (Wrong)
Token
...
최악의 사용자 경험(User experience)을 초래합니다.
최종 아키텍처는 스트리밍과 음성을 분리합니다.
토큰 스트림 (Token Stream)
│
┌─────────────┴─────────────┐
...
구현:
public Flux<VoiceChatEvent> voiceChat(...) {
Flux<String> tokenStream =
...
브라우저는 즉각적으로 업데이트됩니다.
첫 번째 문장이 완성되는 즉시 음성이 시작됩니다.
어느 쪽도 다른 쪽을 차단하지 않습니다.
VoiceChatEvent
SSE 스트림은 강력한 타입(Strongly typed)의 이벤트를 방출합니다.
public record VoiceChatEvent(
EventType type,
String data
...
초기 SESSION 이벤트는 실질적인 문제를 해결합니다.
서버가 완전히 새로운 대화를 생성하면, 프론트엔드는 향후 요청을 위해 생성된 세션 ID(Session ID)를 즉시 수신합니다.
REST API
다섯 개의 엔드포인트(Endpoint)가 음성 시스템을 구동합니다.
POST /api/v1/voice/transcribe
POST /api/v1/voice/speak
POST /api/v1/voice/speak/bytes
...
서로 다른 사용 사례를 위해 두 개의 음성 엔드포인트가 존재합니다.
/speak
- 서버에서 직접 오디오를 재생합니다.
- CLI 사용에 이상적입니다.
/speak/bytes
- WAV 바이트(Bytes)를 반환합니다.
- 브라우저 및 데스크톱 클라이언트를 대상으로 합니다.
우리가 배운 것들 (What We Learned)
Ollama는 오디오 모델을 지원하지 않습니다
원래 계획은 단순히 틀렸습니다.
구현하기 전에 커뮤니티의 피드백을 통해 이를 파악했습니다.
차단 작업(Blocking work)은 격리되어야 합니다
모든 Whisper 요청은 차단(Blocking) 방식입니다.
모든 TTS 프로세스는 차단(Blocking) 방식입니다.
모든 작업은 boundedElastic()에서 실행됩니다.
festival --tts는 파일을 생성할 수 없습니다
오직 오디오를 재생할 뿐입니다.
Linux 오디오 생성에는 다음이 필요합니다:
text2wave -o output.wav
또는 Festival의 Scheme 인터페이스가 필요합니다.
프로세스 정리(Process cleanup)가 중요합니다
if (!process.waitFor(
TIMEOUT_SECONDS,
TimeUnit.SECONDS)) {
...
waitFor()를 무시하면 고아 프로세스(Orphaned child processes)가 남게 됩니다.
DST는 진정으로 어렵습니다
시간대(Timezone) 이름은 단순히 시간대 자체에 의존하는 것이 아니라, 실제 시점(Instant)에 따라 달라집니다.
음성 상태 (Voice Status)
음성 기능을 활성화하기 전에 클라이언트는 사용 가능 여부를 확인할 수 있습니다.
curl http://localhost:8080/api/v1/voice/status \
-H "Authorization: Bearer $TOKEN"
{
"success": true,
"data": {
...
transcriptionMode (전사 모드)
groq-cloudlocal-whisper
ttsEngine (TTS 엔진)
system-windowssystem-macossystem-linux
완전한 음성 대화
사용자가 말함
"카트만두의 날씨는 어때요?"
...
AI 파이프라인(Pipeline)에서 변경되는 것은 아무것도 없습니다.
음성은 단순히 단계 1~4 동안 구축된 아키텍처(Architecture)를 감싸는 역할을 할 뿐입니다.
다음 단계는 무엇인가요?
단계 6에서는 **에이전트 시스템 (Agent System)**을 도입하여, Jarvis가 다단계 작업을 자율적으로 계획하고 실행할 수 있도록 했습니다.
단계 7에서는 우리가 구축한 모든 것에 대한 완전한 웹 인터페이스(Web Interface)를 제공합니다.
이제 백엔드(Backend)가 완성되었습니다.
단계 1~6은 병합되었고, 테스트를 마쳤으며, 프로덕션(Production) 준비가 되었습니다.
이제 Jarvis는 들을 수 있습니다.
이제 Jarvis는 말할 수 있습니다.
기여하기 (Contributing)
Jarvis는 Apache 2.0 라이선스 하에 오픈 소스로 제공됩니다.
현재 기여하기 좋은 이슈(Issues)는 다음과 같습니다:
#69 CLI 음성 명령 (voice, listen, speak)
New 음성 통합 테스트
GitHub:
github.com/sujankim/jarvis-ai-platform
Jarvis AI 플랫폼 시리즈
- 파트 1 — Spring Boot 4를 사용한 로컬 우선(Local-First) AI 어시스턴트 구축
- 파트 2 — pgvector를 사용한 장기 기억(Long-Term Memory) 구축
- 파트 3 — 의미론적 메모리 검색(Semantic Memory Retrieval) 구현
- 파트 4 — Spring AI를 사용한 도구 엔진(Tool Engine) 구축
- 파트 5 — Whisper와 Text-to-Speech를 사용한 음성 기능 추가 (본 문서)
- 파트 6 — ReAct 패턴을 사용한 AI 에이전트 시스템 구축 (다음 예정)
당신의 AI. 당신의 데이터. 당신의 머신.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기