Vertex AI 오디오 전사

Vertex AI 오디오 전사 (Audio Transcription)

Vertex AI의 Gemini speech-to-text와 Chirp 스트리밍 speech-to-text를 LiteLLM에서 사용하는 방법을 알아봐요.

출처: 문서

본문

속성 내용
설명 OpenAI /v1/audio/transcriptions 엔드포인트를 통한 Vertex AI Gemini speech-to-text, /v1/realtime을 통한 Chirp 스트리밍 speech-to-text
LiteLLM 라우트 vertex_ai/gemini-3.5-transcribe-preview (배치), vertex_ai/chirp_3 (realtime, 아래 참조)
지원 OpenAI 파라미터 language, response_format (json, text)

Vertex AI의 Gemini 전사 모델은 global 위치에서 제공되며, LiteLLM은 vertex_location이 설정되지 않을 때 이를 기본으로 사용해요. 지역 위치를 설정하면(구성, litellm.vertex_location, 또는 VERTEXAI_LOCATION/VERTEX_LOCATION env var) 해당 값이 우선하며, 모델이 없는 리전에서는 Vertex가 404를 반환하므로 이 모델들에는 vertex_location: global을 지정하세요.

GCP 서비스 계정보다 API 키가 더 좋나요? 같은 모델이 Google AI Studio에서 gemini/gemini-3.5-transcribe로 제공돼요.

빠른 시작

LiteLLM Python SDK

from litellm import transcription

audio_file = open("speech.wav", "rb")
response = transcription(
    model="vertex_ai/gemini-3.5-transcribe-preview",
    file=audio_file,
    language="en",
    vertex_project="your-project-id",
    vertex_credentials="/path/to/service_account.json",
)
print(response.text)

LiteLLM AI Gateway

config.yaml:

model_list:
  - model_name: gemini-transcribe
    litellm_params:
      model: vertex_ai/gemini-3.5-transcribe-preview
      vertex_project: "your-project-id"
      vertex_credentials: "/path/to/service_account.json"

Proxy 시작:

litellm --config /path/to/config.yaml

요청하기:

curl http://0.0.0.0:4000/v1/audio/transcriptions \
  -H "Authorization: Bearer ***" \
  -F [email protected] \
  -F model=gemini-transcribe \
  -F language=en
import openai

client = openai.OpenAI(api_key="sk-", base_url="http://0.0.0.0:4000")

audio_file = open("speech.wav", "rb")
response = client.audio.transcriptions.create(
    model="gemini-transcribe",
    file=audio_file,
    language="en",
)
print(response.text)

지원 파라미터

language는 두 글자 코드 또는 BCP-47 태그를 받으며, Gemini로 보내기 전에 BCP-47로 정규화돼요. response_formatjson(기본값)과 text를 지원해요. Vertex AI의 Gemini 전사는 단어 타임스탬프를 반환하지 않으므로 verbose_json, srt, vttdrop_params가 설정되지 않으면 400으로 거부되며, 설정 시 삭제되고 일반 전사만 반환돼요.

사용법 및 비용 추적

Vertex AI는 모달리티별로 분리된 토큰 사용량을 보고하며, LiteLLM은 오디오 입력 토큰과 텍스트 출력 토큰을 별도로 추적하므로 스펜드 로그와 x-litellm-response-cost 헤더가 Google이 발표한 토큰당 오디오 전사 가격을 반영해요.

Chirp 실시간 전사

LiteLLM은 proxy의 OpenAI 호환 /v1/realtime 엔드포인트를 통해 Google Cloud Speech-to-Text Chirp 모델을 제공해요. 클라이언트는 OpenAI Realtime 전사 프로토콜로 말하고, LiteLLM은 PCM16 오디오를 gRPC로 Speech-to-Text v2 StreamingRecognize에 스트리밍하며 Google의 중간·최종 결과를 OpenAI 이벤트로 매핑해요. 아래 Python 예시는 OpenAI SDK를 사용하는 완전한 push to talk 세션 예시예요.

1. Speech-to-Text 클라이언트 설치

스트리밍은 stt-vertex-chirp extra가 설치하는 google-cloud-speech를 사용해요. 공식 Docker 이미지에는 이미 포함돼 있어요.

pip install 'litellm[stt-vertex-chirp]'

없으면 첫 세션이 google-cloud-speech is not installed로 실패해요.

2. config에 모델 추가

config.yaml:

model_list:
  - model_name: chirp-3
    litellm_params:
      model: vertex_ai/chirp_3
      vertex_project: "your-project-id"
      vertex_location: us
      vertex_credentials: "/path/to/service_account.json"

vertex_credentialsVERTEXAI_CREDENTIALS 환경 변수로 폴백하고, vertex_projectvertex_locationlitellm.vertex_project·litellm.vertex_location, 그다음 VERTEXAI_PROJECT·VERTEXAI_LOCATION으로 폴백해요. 위치는 기본값 us이며 지역 엔드포인트 -speech.googleapis.com(global이면 speech.googleapis.com)을 선택하므로, Speech-to-Text가 모델을 서비스하는 위치를 골라야 해요. api_base는 선택이며 host만 사용돼요. Chirp 배포에서는 realtime health check가 지원되지 않으므로, 같은 배포가 제공하는 배치 /v1/audio/transcriptions 엔드포인트를 거치는 mode: audio_transcription으로 health check해요.

LiteLLM Proxy 시작:

litellm --config /path/to/config.yaml

3. 연결

ws://localhost:4000/v1/realtime?model=chirp-3&intent=transcription

Authorization: Bearer ***로 인증해요. model은 config의 model_name이며 필수이고, intent=transcription만 있으면 proxy의 기본 OpenAI 전사 모델로 라우팅돼요. intent=transcription은 세션을 전사 전용으로 만들고 session.update에 있는 어떤 모델이든 인증받은 모델로 고정해요.

LiteLLM은 소켓이 받아들여지는 즉시 object: realtime.transcription_sessionsession.created를 내보내요. 기본값을 담고 있어요: 모델 chirp_3, 모노 audio/pcm 24000 Hz, server VAD, 언어 없음.

4. 세션 구성

session.update(또는 transcription_session.update)를 하나 보내요. LiteLLM은 정규화된 세션을 반향하는 session.updated로 응답하며, 이후 업데이트는 무시돼요. 업데이트 전에 보낸 오디오는 버퍼링됐다가 그 후 재생되므로 바로 스트리밍을 시작할 수 있어요.

{
  "type": "session.update",
  "session": {
    "type": "transcription",
    "audio": {
      "input": {
        "format": {"type": "audio/pcm", "rate": 24000, "channels": 1},
        "transcription": {"model": "chirp_3", "language": "en"},
        "turn_detection": {"type": "server_vad"}
      }
    }
  }
}

필드 규칙:

필드 규칙
type transcription, realtime 또는 생략. 그 외는 거부
format typeaudio/pcm(또는 pcm16), rate8000~48000의 정수, channels가 있으면 1. 생략 시 모노 24kHz
transcription.model 선택. intent=transcription이면 proxy가 인증받은 모델로 교체. 없으면 chirp_3 또는 vertex_ai/chirp_3만 허용
transcription.language 선택 BCP-47 태그. 일반 언어의 두 글자 코드는 기본 리전으로 확장(enen-US, ptpt-BR, zhzh-CN), 그 외는 주어진 대로 전송. 생략 시 Google이 언어 감지
turn_detection 모드를 선택(아래 표). server_vad 외의 type은 거부. type이 없는 객체는 server VAD
기타 transcription 디버그 로그 줄과 함께 무시

베타 레이아웃: input_audio_format: "pcm16" + 최상위 input_audio_transcriptionturn_detection은 24kHz에서 허용돼요. 하나의 업데이트에서 두 레이아웃을 섞지 마세요.

Push to talk Server VAD
turn_detection null 생략, 또는 {"type": "server_vad"}
턴 경계 commit당 한 턴. 언제 끝날지는 사용자가 결정 Google이 발화 감지. 각 발화는 고유 item_id
Speech events 없음 각 발화 주변에 input_audio_buffer.speech_startedspeech_stopped
턴 종료 input_audio_buffer.commit(또는 input_audio_buffer.end)이 턴을 끝내고 completed 산출. 다음 append가 새 턴 시작 발화는 스스로 완료되며, commit 또는 end는 진행 중인 턴을 완료

잘못된 session.update(잘못된 인코딩, 채널 수, 샘플 레이트, 세션 타입, 턴 감지 타입, 다른 모델)는 코드 1006으로 연결을 닫고 error 이벤트는 없어요. 이유는 디버그 레벨로 Error in client ack messages: ...에 기록돼요. append의 잘못된 base64나 홀수 PCM 바이트도 같은 방식으로 닫혀요.

5. 오디오 스트리밍

input_audio_buffer.append는 설정된 rate의 base64 모노 PCM16을 운반해요. 이벤트당 크기 제한은 없으며, LiteLLM은 오디오를 최대 25 KB의 gRPC 요청으로 나누어 도착하는 대로 페이싱 없이 전달하므로 실시간보다 빠르게 파일을 보낼 수 있어요. input_audio_buffer.clear는 진행 중인 턴(이미 보낸 오디오와 아직 완료되지 않은 텍스트 포함)을 이벤트 없이 버려요. response.create를 포함한 다른 모든 클라이언트 이벤트는 삭제돼요.

Google은 하나의 스트리밍 요청이 실행될 수 있는 시간을 제한하므로, LiteLLM은 스트림이 240초 열려 있으면 새 스트림으로 이동해요. 말하기의 다음 휴지(Google의 voice activity end)를 기다렸다가 전환하고, 늦어도 280초에는 전환해요. push-to-talk 턴은 전환을 가로질러 유지돼요. 지금까지의 텍스트를 유지하고 commit 후의 completed가 전체 턴을 하나의 item_id로 담아요. server VAD에서는 기존 스트림이 닫힐 때 Google이 지금까지 보낸 오디오를 최종화하므로, 강제 전환 시 진행 중이던 발화(240~280초 연속 말하기)는 두 item_id 아래 두 부분으로 완료돼요. 과금된 초는 스트림을 가로질러 이어져요.

6. 전사 읽기

이벤트 필드 시점
session.created session.object: realtime.transcription_session, 기본 세션 연결 시
session.updated 정규화된 세션: channels 없음, modelchirp_3, language는 BCP-47 태그(enen-US) 또는 없음 LiteLLM이 session.update 승인 후
input_audio_buffer.speech_started item_id server VAD 전용: Google이 말소리를 감지하거나 턴의 첫 전사 텍스트 도착
conversation.item.input_audio_transcription.delta item_id, content_index: 0, delta 턴의 이전 delta 이후 추가된 단어 (중간 결과 포함)
input_audio_buffer.speech_stopped item_id server VAD 전용: Google이 발화 끝 감지 또는 턴 완료
conversation.item.input_audio_transcription.completed item_id, content_index: 0, transcript; usage: {"type": "duration", "seconds": 18.0} (이전 completed 이후 Google이 오디오를 과금한 경우) server VAD: 각 최종 결과(연속 말하기 중 강제 스트림 전환 포함). Push to talk: commit 또는 end
error error.type: server_error; error.messageupstream websocket closed with code 1011: Google Speech-to-Text streaming failed: ... Google의 스트림이 실패. proxy는 직후 코드 1011로 종료

델타는 단어 단위로, 대소문자와 구두점을 무시하고 계산돼요. 그래서 Google이 이전 단어를 수정하면 delta가 그 단어에서 다시 시작해요. 최종 텍스트는 delta를 합치는 대신 Google의 최종 결과인 completed.transcript에서 가져오세요. 턴은 item_id로 상관관계가 매겨져요. 마지막 completed 후 세션은 사용자가 닫을 때까지 열려 있고, proxy는 Google 실패 후에만 스스로 닫아요.

가격 및 사용량

Chirp는 오디오 1초당 과금돼요(모델 비용 맵의 input_cost_per_second). usage.seconds는 이전 completed 이후 Google이 과금한 오디오(침묵 포함)이므로 발화 길이를 초과할 수 있어요. 마지막 completed 이벤트 후 과금된 오디오(예: 꼬리 침묵)는 세션 종료 시 비용으로 기록돼요.

전사본은 메시지 로깅이 꺼져 있지 않으면 스펜드 로그의 messages에 저장돼요.

가드레일

mode: realtime_input_transcription 가드레일이 각 완성된 전사에서 실행돼요. completed 이벤트는 가드레일 전에 클라이언트에 도달하며, 델타는 검사되지 않아요. 연결마다 guardrails=name1,name2 쿼리 파라미터로 가드레일을 선택할 수 있어요.

warning: realtime_input_transcription 가드레일을 구성하면 LiteLLM이 세션 구성 전에 클라이언트의 turn_detection: null{"create_response": false}로 다시 작성해요. 따라서 push to talk 클라이언트는 server VAD 모드에서 실행되며, 발화는 스스로 완료되고 input_audio_buffer.commit은 진행 중인 턴만 완료해요. Server VAD 클라이언트는 영향을 받지 않아요.

예시 OpenAI SDK 클라이언트

Push to talk: OpenAI SDK로 연결하고, 구성한 뒤 100ms 청크의 모노 PCM16 WAV 파일을 스트리밍하고, commit한 뒤 최종 전사가 도착할 때까지 이벤트를 읽어요.

import asyncio
import base64
import wave

from openai import AsyncOpenAI

client = AsyncOpenAI(base_url="http://localhost:4000/v1", api_key="sk-1234")

def pcm_chunks(path, chunk_ms=100):
    with wave.open(path) as w:
        rate, frames = w.getframerate(), w.readframes(w.getnframes())
    step = rate * 2 * chunk_ms // 1000
    return rate, [frames[i : i + step] for i in range(0, len(frames), step)]

async def main():
    rate, chunks = pcm_chunks("speech.wav")
    async with client.realtime.connect(model="chirp-3", extra_query={"intent": "transcription"}) as conn:
        await conn.session.update(
            session={
                "type": "transcription",
                "audio": {
                    "input": {
                        "format": {"type": "audio/pcm", "rate": rate},
                        "transcription": {"model": "chirp-3", "language": "en"},
                        "turn_detection": None,
                    }
                },
            }
        )
        for chunk in chunks:
            await conn.input_audio_buffer.append(audio=base64.b64encode(chunk).decode())
            await asyncio.sleep(0.1)
        await conn.input_audio_buffer.commit()
        async for event in conn:
            if event.type == "conversation.item.input_audio_transcription.delta":
                print(event.delta, end="", flush=True)
            elif event.type == "conversation.item.input_audio_transcription.completed":
                print(f"\n{event.transcript} ({event.usage.seconds} s)")
                break
            elif event.type == "error":
                print(event.error.message)
                break
            else:
                print(event.type)

asyncio.run(main())
session.created
session.updated
what is the weather in Paris
What is the weather in Paris? (2.4 s)

server VAD의 경우 None 대신 "turn_detection": {"type": "server_vad"}를 보내고 마이크가 열려 있는 동안 계속 이벤트를 읽어요. 각 감지된 발화는 고유 item_id와 함께 오고 스스로 완료되며, input_audio_buffer.commit은 진행 중인 것을 완료해요.

더 알아보기 (Learn more)

  • Gemini 전사 문서
  • Cloud Speech-to-Text 문서