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_format은 json(기본값)과 text를 지원해요. Vertex AI의 Gemini 전사는 단어 타임스탬프를 반환하지 않으므로 verbose_json, srt, vtt는 drop_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_credentials는 VERTEXAI_CREDENTIALS 환경 변수로 폴백하고, vertex_project와 vertex_location은 litellm.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_session인 session.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 |
type은 audio/pcm(또는 pcm16), rate는 8000~48000의 정수, channels가 있으면 1. 생략 시 모노 24kHz |
transcription.model |
선택. intent=transcription이면 proxy가 인증받은 모델로 교체. 없으면 chirp_3 또는 vertex_ai/chirp_3만 허용 |
transcription.language |
선택 BCP-47 태그. 일반 언어의 두 글자 코드는 기본 리전으로 확장(en → en-US, pt → pt-BR, zh → zh-CN), 그 외는 주어진 대로 전송. 생략 시 Google이 언어 감지 |
turn_detection |
모드를 선택(아래 표). server_vad 외의 type은 거부. type이 없는 객체는 server VAD |
기타 transcription 키 |
디버그 로그 줄과 함께 무시 |
베타 레이아웃: input_audio_format: "pcm16" + 최상위 input_audio_transcription과 turn_detection은 24kHz에서 허용돼요. 하나의 업데이트에서 두 레이아웃을 섞지 마세요.
| Push to talk | Server VAD | |
|---|---|---|
turn_detection |
null |
생략, 또는 {"type": "server_vad"} |
| 턴 경계 | commit당 한 턴. 언제 끝날지는 사용자가 결정 | Google이 발화 감지. 각 발화는 고유 item_id |
| Speech events | 없음 | 각 발화 주변에 input_audio_buffer.speech_started와 speech_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 없음, model은 chirp_3, language는 BCP-47 태그(en → en-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.message는 upstream 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 문서