음성 인터페이스는 AI Agent 서비스의 접근성을 한 단계 끌어올립니다. 이 모듈에서는 ElevenLabs의 Scribe v2 STT와 eleven_v3 TTS를 조합해 음성 → 텍스트 → 내 목소리 음성으로 돌아오는 풀 파이프라인을 직접 구현합니다.
ElevenLabs Scribe v2 STT
Scribe v2는 ElevenLabs가 제공하는 REST 기반 STT 모델로, 화자분리(diarization)와 단어 수준 타임스탬프를 지원합니다. SDK 없이 fetch로 직접 호출합니다.
// app/api/stt/route.ts
export async function POST(req: Request) {
const formData = await req.formData();
const audioFile = formData.get("audio") as File;
const body = new FormData();
body.append("audio", audioFile);
body.append("model_id", "scribe_v2");
body.append("diarize", "true"); // 화자분리 활성화
body.append("timestamps_granularity", "word"); // 단어별 타임스탬프
const res = await fetch("https://api.elevenlabs.io/v1/speech-to-text", {
method: "POST",
headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
body,
});
if (!res.ok) throw new Error(`STT failed: ${res.status}`);
const data = await res.json();
// data.words: [{ text, start, end, speaker_id }]
return Response.json({ transcript: data.text, words: data.words });
}
응답의 words 배열에는 speaker_id(화자 구분)와 start/end(초 단위 타임스탬프)가 함께 담깁니다. 회의록 자동 생성이나 다자 대화 분석에 바로 활용할 수 있습니다.
cloud_storage_url 패턴 — 서버 우회
오디오 파일이 크면(예: 30분 녹음) 서버를 경유하는 것은 1GB 제한과 Cold Start 타임아웃 위험을 함께 안고 갑니다. ElevenLabs는 audio_url 파라미터로 Vercel Blob의 public URL을 직접 넘기는 패턴을 지원합니다.
// 클라이언트가 먼저 Vercel Blob에 업로드 → URL만 API로 전달
const blobUrl = await uploadToBlob(audioFile); // Vercel Blob 퍼블릭 URL
const body = new FormData();
body.append("model_id", "scribe_v2");
body.append("audio_url", blobUrl); // 파일 대신 URL
body.append("diarize", "true");
const res = await fetch("https://api.elevenlabs.io/v1/speech-to-text", {
method: "POST",
headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
body,
});
이 패턴을 쓰면 Next.js API Route는 파일 바이트를 전혀 다루지 않아도 됩니다. 파일 → Blob → URL → ElevenLabs 흐름이 핵심입니다.
주의: Vercel Blob URL은 기본적으로 공개입니다. 민감한 음성 데이터라면 처리 완료 후 즉시 삭제(
del)하거나 signed URL 패턴을 검토하세요.
eleven_v3 TTS와 음성 클로닝
TTS 엔드포인트는 /v1/text-to-speech/{voice_id}입니다. optimize_streaming_latency=3은 지연시간과 품질의 균형점으로, 실시간 응답이 필요한 Agent 서비스에서 권장 설정입니다.
음성 클로닝 절차: ElevenLabs 대시보드(또는 Instant Voice Clone API)에 30초 이상의 깨끗한 샘플을 올리면 voice_id가 발급됩니다. 이 ID를 환경변수(MY_VOICE_ID)로 관리합니다.
// app/api/tts/route.ts
export async function POST(req: Request) {
const { text } = await req.json();
const res = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${process.env.MY_VOICE_ID}` +
`?optimize_streaming_latency=3`,
{
method: "POST",
headers: {
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
model_id: "eleven_v3",
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
}),
}
);
if (!res.ok) throw new Error(`TTS failed: ${res.status}`);
// 오디오 스트림을 그대로 클라이언트로 전달
return new Response(res.body, {
headers: { "Content-Type": "audio/mpeg" },
});
}
클라이언트에서는 응답 Blob을 HTMLAudioElement에 물려 자동재생합니다.
// components/AudioPlayer.tsx (일부)
const playTTS = async (text: string) => {
const res = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.play(); // 사용자 제스처 후 호출 필요
};
브라우저 정책:
audio.play()는 사용자 인터랙션(버튼 클릭 등) 이후에만 허용됩니다. 페이지 로드 직후 자동재생은 대부분의 브라우저에서 차단됩니다.
모델 비교
| 항목 | Scribe v2 (STT) | eleven_v3 (TTS) |
|---|---|---|
| 방식 | REST POST (multipart or URL) | REST POST → 오디오 스트림 |
| 지연 | 파일 길이에 비례 | latency=3 기준 ~400ms |
| 주요 기능 | 화자분리, 단어 타임스탬프 | 감정 표현, 클로닝 지원 |
| 요금 단위 | 오디오 분(minute) | 문자 수(character) |
실습 (Lab)
목표: 음성 메모를 녹음하고, STT로 텍스트화한 뒤, 본인 보이스로 요약 음성을 재생하는 엔드-투-엔드 흐름을 완성합니다.
단계 1 — 음성 클로닝 준비
- ElevenLabs 대시보드 → "Voices" → "Add a new voice" → "Instant Voice Cloning" 선택.
- 30초 이상 잡음 없이 본인 목소리로 녹음한 파일을 업로드.
- 생성된
voice_id를 복사해.env.local에MY_VOICE_ID=...로 저장.
단계 2 — 녹음 UI 구현
MediaRecorder API로 브라우저 마이크 입력을 Blob으로 수집합니다. 녹음 완료 시 /api/stt로 POST합니다.
단계 3 — STT 라우트 연결
위 app/api/stt/route.ts 코드를 프로젝트에 추가합니다. 파일이 1MB 초과이면 먼저 Vercel Blob에 업로드하고 cloud_storage_url 패턴으로 전환합니다.
단계 4 — 요약 → TTS 재생
STT 결과 텍스트를 callLLM()으로 넘겨 3줄 요약을 생성(temperature: 0.2)한 뒤, /api/tts로 보내 오디오를 받아 HTMLAudioElement로 재생합니다.
단계 5 — 화자분리 결과 표시
words 배열을 파싱해 speaker_id별로 색을 달리한 트랜스크립트 UI를 렌더링합니다(예: speaker_0 → 파란색, speaker_1 → 주황색).
검증 기준: 녹음 버튼 클릭 → 말하기 → 중지 → 텍스트 표시 → "요약 재생" 클릭 → 본인 목소리로 요약 재생. 이 흐름이 에러 없이 완료되면 실습 성공입니다.
핵심 정리
- Scribe v2 STT는 SDK 없이
fetch로 직접 호출하며,diarize=true로 화자분리,timestamps_granularity=word로 단어별 타임스탬프를 얻는다. - 대용량 오디오는 Vercel Blob에 먼저 올린 뒤
audio_url로 넘기는 cloud_storage_url 패턴으로 서버 부하를 완전히 우회한다. - eleven_v3 TTS는
optimize_streaming_latency=3으로 호출해 응답 스트림을Content-Type: audio/mpeg로 클라이언트에 바로 전달한다. - 음성 클로닝은 30초 샘플만으로 가능하며, 발급된
voice_id를 환경변수로 관리한다. HTMLAudioElement.play()는 반드시 사용자 인터랙션 이후에 호출해야 브라우저 자동재생 정책을 통과한다.- STT 결과를
callLLM()에 넘길 때는temperature: 0.2를 유지해 결정적 요약을 생성한다.
음성 메모 녹음 → 텍스트 변환 → 자신의 보이스로 음성 보고