← Lecture Note
M11실습60분 · ~16 슬라이드

STT / TTS 통합 (ElevenLabs)

내 목소리로 보고하는 에이전트

학습 목표

음성→텍스트→내 보이스 음성으로 도는 풀 음성 파이프라인을 구축한다.

음성 인터페이스는 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 — 음성 클로닝 준비

  1. ElevenLabs 대시보드 → "Voices" → "Add a new voice" → "Instant Voice Cloning" 선택.
  2. 30초 이상 잡음 없이 본인 목소리로 녹음한 파일을 업로드.
  3. 생성된 voice_id를 복사해 .env.localMY_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를 유지해 결정적 요약을 생성한다.
LAB · 실습

음성 메모 녹음 → 텍스트 변환 → 자신의 보이스로 음성 보고