본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 03. 17:35

OpenClaw 음성 튜토리얼 노트

요약

음식 주문 에이전트 구현을 위한 OpenClaw 음성 튜토리얼 보조 자료입니다. 채팅 중심의 기본 설정을 음성 인터페이스에 최적화하기 위한 Deepgram 파라미터 조정, 블록 스트리밍 설정 변경, 오디오 파이프라인 버그 해결 방법을 다룹니다.

핵심 포인트

  • OpenClaw를 프롬프트가 아닌 상태를 유지하는 런타임으로 취급
  • 음성 인터페이스를 위한 블록 스트리밍 및 파라미터 최적화 방법
  • 오디오 프레임 처리 및 도구 호출 지연(dead air) 해결 가이드
  • Node.js 및 OS별 네이티브 오디오 의존성 설치 요구사항

이 글은 음식 주문 에이전트(food-ordering agent) 튜토리얼 영상의 보조 자료입니다. 영상에서는 저장소(repo)를 클로닝(cloning)하고 음성으로 실제 Swiggy 주문을 넣는 과정을 안내합니다. 이 포스트는 영상에서 언급되었지만 다룰 시간이 없었던 다음 내용들을 보완합니다:

  1. 모든 Deepgram Flux 파라미터(parameter), 기능 및 이벤트 모델(event model)의 동작 방식
  2. OpenClaw의 블록 스트리밍(block streaming) 기본 설정이 음성(voice)에 부적합한 이유와 변경해야 할 설정
  3. Falcon 음성 및 로케일(locale) 호환성, 그리고 시스템을 망가뜨리지 않고 음성을 교체하는 방법
  4. 설정 후 발생하는 스트리밍 파이프라인(streaming-pipeline) 버그와 그 근본 원인

Repo: https://github.com/murf-ai/murf-cookbook/tree/main/examples/openclaw/food_ordering_agent

OpenClaw는 에이전트를 프롬프트(prompt)가 아닌 런타임(runtime)으로 취급합니다. 런타임은 서버와 같이 지속적으로 실행되며 호출 간의 상태(state)를 기억하는 프로그램입니다. 반면, 프롬프트는 모델에 전송되는 단일 텍스트 블록입니다. OpenClaw는 여러 턴(turn)에 걸쳐 세션을 일시 중지, 재개 및 추적할 수 있기 때문에 이 차이가 중요합니다.

이 모델은 채팅(chat)에는 잘 작동합니다. 하지만 음성(voice)에서는 문제가 발생하기 시작합니다.

마이크는 텍스트를 생성하지 않습니다. 마이크는 오디오 프레임(audio frames, 가공되지 않은 소리 데이터의 작은 조각)을 생성합니다. 스피커는 재생을 시작하기 전에 전체 답변이 완료될 때까지 기다릴 수 없습니다. 사용자는 침묵을 듣게 되고 에이전트가 고장 났다고 생각할 것입니다. 채팅 UI에서는 보이지 않던 도구 호출(tool-call) 지연이, 사용자가 소리를 들을 수 있는 순간에는 명백한 무음 상태(dead air)로 나타납니다.

OpenClaw의 모든 구성 요소는 음성에서도 여전히 작동합니다. 다만 채팅 친화적인 기본 설정(defaults)에 의존하는 대신, 각 구성 요소를 의도적으로 음성 유스케이스(use case)에 맞춰 지정해야 합니다. 다음 세 섹션에서는 어떤 기본 설정을 왜 변경해야 하는지 살펴봅니다.

요구 사항 (Requirements)

아래 항목이 없다면 계속 진행하기 전에 설정하십시오.

Node 및 패키지 매니저 (package manager)

  • Node 22.16 이상. 이 저장소(repo)는 ESM 전용이며, 이전 버전에서는 작동하지 않습니다.
  • pnpm 9 이상. 락파일(lockfile)은 pnpm을 사용합니다. npm이나 yarn을 사용하면 서로 다른 버전이 결정될 수 있습니다.

플랫폼 오디오 의존성 (Platform audio dependencies)

Decibri는 각 운영 체제의 네이티브 오디오 스택을 사용하므로, 설치 단계가 다릅니다.

  • Linux: Debian 계열 배포판에서는 apt install libasound2-dev를, Fedora 계열에서는 alsa-lib-devel을 설치하십시오. 설치 시점에 필요합니다.
  • Windows: WASAPI가 내장되어 있습니다. Decibri 바이너리를 위해 C++ 빌드 툴체인(build toolchain)이 필요합니다. Visual Studio Installer를 통해 "C++를 사용한 데스크톱 개발"을 설치하십시오.
  • macOS: CoreAudio가 내장되어 있습니다. Xcode 명령줄 도구(Xcode Command Line Tools)가 필요합니다: xcode-select --install.

외부 CLI (External CLIs)

  • clawhub. OpenClaw의 스킬 레지스트리(skill registry)입니다. 이 저장소의 Swiggy 스킬은 벤더링(vendored)되어 있으므로, 에이전트를 실행하는 데 반드시 clawhub가 필요한 것은 아니지만, 나중에 다른 스킬을 가져오려면 필요합니다.

API 키 (API keys)

  • Flux STT 키를 위한 Deepgram. 신규 계정은 카드 등록 없이 $200의 스타터 크레딧을 받습니다.
  • Falcon TTS 키를 위한 Murf. 이는 일반적인 Murf Studio 계정과는 별개로, Murf 계정의 API 탭에서 생성됩니다.
  • 원하는 LLM 제공업체. 대부분의 업체는 개발에 충분한 무료 티어(free tier)를 제공합니다.

Swiggy

최소 하나 이상의 저장된 배송 주소가 있는 Swiggy 계정. MCP 인터페이스가 좌표가 아닌 주소를 노출하기 때문에, 에이전트는 실시간 GPS가 아닌 저장된 주소로 주문합니다.

업데이트: Swiggy 인증 흐름이 영상 녹화 이후 변경되었습니다. mcporter auth swiggy-food는 더 이상 작동하지 않습니다. 현재 Swiggy MCP는 승인된 client_id를 요구하며 대신 커스텀 PKCE 스크립트를 사용합니다. node scripts/swiggy-auth.mjs를 실행하십시오. 현재 단계는 저장소 README를 참조하십시오.

Deepgram Flux

Deepgram Flux

Flux는 이번 빌드에서 사용하는 STT (Speech-to-Text, 음성 인식)입니다. 음성 에이전트(voice agents)에 사용할 수 있는 여러 스트리밍 STT가 있지만, 여기에는 Flux가 연결되어 있습니다. 아래의 구성 요소들은 어떤 API를 선택하든 올바르게 설정해야 하는 부분들입니다.

매개변수(parameters)를 살펴보기 전에 다룰 만한 개념이 하나 있습니다: 발화 전환 (turn-taking). 이는 사용자가 말을 멈추고 에이전트가 응답해야 하는 시점을 결정하는 것입니다. 많은 스트리밍 STT API는 부분적인 전사(partial transcripts)를 반환하고 발화 전환 처리를 사용자의 코드에 맡기는데, 이는 보통 침묵을 감지하는 별도의 VAD (Voice Activity Detector, 음성 활동 감지기)를 추가해야 함을 의미합니다. Flux는 전사 모델 내부에서 발화 전환을 수행하고 이를 위한 구조화된 이벤트를 방출하므로, 이번 빌드에서는 별도의 VAD가 필요하지 않습니다.

엔드포인트 (Endpoint)

엔드포인트는 서버에서 연결하는 URL 경로입니다. Flux는 /v2/listen에서만 작동합니다. 이전 버전인 /v1/listen 엔드포인트는 모델 매개변수를 조용히 거부할 것입니다. 그러면 왜 아무것도 전사되지 않는지 한 시간 동안 고민하게 될 것입니다.

const params = new URLSearchParams();
params.append("model", "flux-general-en");
params.append("encoding", "linear16");
...

URL을 생성할 때는 URLSearchParams를 사용하십시오. 이는 여러 단어로 된 키워드의 공백을 올바르게( + 로) 인코딩합니다. 만약 쿼리 문자열(query string)을 수동으로 작성하여 %20을 사용한다면, Deepgram은 이유를 알려주지 않고 연결을 종료할 것입니다. 이것이 가장 흔하게 발생하는 설정 버그입니다.

매개변수 (Parameters)

아래의 오디오 형식은 PCM이라는 용어를 사용하는데, 이는 펄스 부호 변조 (pulse-code modulation)를 의미합니다. 이는 원시 오디오(raw audio)를 숫자로 표현하는 표준 방식입니다. linear16은 각 샘플이 리틀 엔디언 (little-endian) 바이트 순서로 저장된 16비트 숫자임을 의미합니다. 대부분의 오디오 라이브러리는 기본적으로 이 형식을 사용합니다.

매개변수사용된 값역할
modelflux-general-enFlux 영어. 다국어를 위해서는 flux-general-multi를 사용하십시오.
...

또한 eot_threshold를 전달하여 발화 종료 (end-of-turn) 민감도를 조정할 수 있습니다. 기본값은 짧은 음식 주문 문장에 잘 작동합니다. 만약 에이전트가 더 긴 혼잣말(thinking-out-loud utterances)을 처리해야 한다면, 이 값을 높이십시오.

우리가 사용하는 Flux 이벤트

Flux는 TurnInfo 스트림을 통해 다섯 가지 이벤트 유형을 전송합니다. 이 저장소(repo)는 그중 하나만 소비하지만, 다른 이벤트들도 나중에 필요할 가능성이 높으므로 알아두는 것이 좋습니다.

  • Update. 사용자가 계속 말을 함에 따라 업데이트되는 부분 전사(Partial transcript)입니다. 실시간 전사 디스플레이를 원하는 경우 유용합니다. 여기서는 사용되지 않습니다.
  • StartOfTurn. 사용자가 막 말을 시작했습니다. 이 시점에서 끼어들기(barge-in, 에이전트가 아직 말하고 있다면 사용자가 방해할 수 있도록 중단시키는 것)를 처리할 수 있습니다. 여기서는 연결되지 않았습니다.
  • EndOfTurn. 사용자가 말을 마쳤다는 높은 확신(High confidence)을 나타냅니다. 이 저장소가 사용하는 유일한 이벤트입니다. 이 이벤트가 발생하면 전사 내용이 LLM으로 전달되고 에이전트가 답변 생성을 시작합니다.
  • EagerEndOfTurn. 사용자가 말을 마쳤다는 중간 정도의 확신(Medium confidence)을 나타냅니다. 기본적으로는 꺼져 있습니다. 만약 이를 켠다면(eager_eot_threshold 사용), 에이전트가 답변 초안을 더 일찍 작성하기 시작할 수 있습니다. 일부 초안이 버려짐에 따라 더 많은 LLM 호출이 발생하는 비용이 들지만, 지연 시간(delay)을 일부 줄일 수 있습니다.
  • TurnResumed. EagerEndOfTurn 이후에만 발생합니다. 사용자가 실제로 말을 마친 것이 아니라는 의미이며, 시작했던 모든 초안은 폐기되어야 합니다.
if (data.type === "TurnInfo") {
  if (data.event === "EndOfTurn") {
    const transcript: string = data.transcript ?? "";
...

인도식 영어 음식 어휘를 위한 핵심 용어 편향 (Keyterm biasing)

Deepgram은 연결당 최대 100개의 핵심 용어(keyterms)를 전달할 수 있게 해줍니다. 핵심 용어는 모델에게 "이 단어들 중 하나와 유사한 소리가 들리면, 이 철자 쪽으로 기울여라"라고 알려주는 역할을 합니다. 대부분의 앱은 연결 시점에 고정된 어휘를 사용하여 핵심 용어를 한 번 설정합니다.

Flux의 Configure 제어 메시지를 사용하면 매 턴(turn)마다 핵심 용어를 업데이트할 수 있습니다. 이 저장소는 에이전트가 방금 말한 고유 명사(proper nouns)에 따라 다음 턴의 편향을 조정하는 데 이를 사용합니다.

function extractContextualKeyterms(text: string): string[] {
  const tokens = text
    .replace(/[.,!?;:()"']/g, " ")
...

아이디어는 간단합니다. 만약 에이전트가 "Paneer Butter Masala from Punjab Grill"이라고 말했다면, 사용자의 답변에는 무작위 식당 이름보다 해당 단어들이 포함될 가능성이 훨씬 높습니다. 따라서 우리는 에이전트의 마지막 답변에서 대문자로 시작하는 단어들을 추출하여 다음 턴(turn)의 편향(bias)으로 사용합니다.

표준 영어 음성 인식(speech recognition)이 가장 어려움을 겪는 인도식 영어 음식 어휘의 경우, 이 기능 하나가 에이전트가 "Kadhai Paneer"를 듣느냐, 아니면 "car die panel"로 듣느냐의 차이를 만듭니다.

비용

Deepgram은 스트리밍 오디오의 초당 사용량을 기준으로 Flux에 비용을 청구합니다. 2026년 초 기준으로, 종량제(pay-as-you-go) 요금은 플랜과 지역에 따라 분당 $0.0077에서 $0.015 사이입니다. 현재 수치는 Deepgram의 가격 페이지를 확인하세요. 신규 계정에는 $200의 스타터 크레딧(starter credit)이 제공됩니다.

음식 주문 에이전트의 대략적인 비용 추정치는 다음과 같습니다:

  • 평균 턴(turn): 사용자 음성 3초, 사용자 음성 중에만 마이크 활성화
  • 턴당 STT(Speech-to-Text) 비용: 범위의 높은 쪽을 기준으로 3초 적용 시 약 $0.00075
  • 10턴 주문 세션: STT 비용 1센트 미만

테스트를 위한 인내심이 바닥나기 훨씬 전에 $200의 크레딧이 먼저 바닥날 것입니다.

블록 스트리밍 (Block streaming)

OpenClaw는 채팅을 우선적으로 고려하여 구축되었습니다. OpenClaw의 블록 스트리밍은 화면상의 긴 답변에 맞춰 조정되었습니다. 그러한 설정에서는 각 블록(모델이 다시 보내는 텍스트 단위)이 문단 전체일 수 있습니다. 음성의 경우, 각 블록은 한두 문장이어야 합니다. "LLM이 생성한 텍스트"와 "스피커가 소리를 재생함" 사이의 모든 밀리초(millisecond)는 사용자가 들을 수 있는 침묵입니다.

기본 설정은 음성에 적합하지 않습니다. 설정을 변경하기 전까지 OpenClaw는 블록을 즉시 코드로 보내는 대신 조용히 붙잡고 있습니다.

먼저, 스트리밍을 켜세요

OpenClaw에는 블록 스트리밍을 제어하는 두 가지 설정이 있습니다:

  • 설정(config) 내의 blockStreamingDefault (채널 전체 기본값)
  • 호출 지점(call site)의 disableBlockStreaming (단일 호출에 대한 오버라이드)

두 설정 모두 스트리밍을 허용해야 스트리밍이 작동합니다.

const llmCall = getReplyFromConfig(ctx, {
  disableBlockStreaming: false,
});

명칭이 혼란스럽습니다. 옵션 이름이 disable(비활성화)이기 때문에, false는 "비활성화하지 않음"을 의미합니다. 즉, "스트리밍을 수행함"을 뜻합니다. 따라서 disableBlockStreaming: false로 설정해야 합니다. 필요하다면 두 번 읽어보세요.

coalescer(결합기) 수정하기

coalescer(결합기)는 버퍼링된 블록(buffered block)을 언제 코드에 보낼지 결정하는 컴포넌트입니다. 버퍼링(buffer)한다는 것은 충분한 양이 쌓일 때까지 무언가를 붙잡고 있는 것을 의미합니다. 버퍼링된 콘텐츠를 다음으로 전달하는 것을 flush(플러시)라고 합니다.

coalescer의 기본 minChars 설정값은 800입니다. 일반적인 음성 답변은 200~300자 정도입니다. 따라서 기본 설정 상태에서는 coalescer가 결코 도달하지 않을 800자 블록을 기다리게 됩니다. 그러다 답변이 끝날 때쯤 포기하고 모든 내용을 한꺼번에 쏟아냅니다. 스트리밍이 실패한 것입니다.

다음과 같이 오버라이드(override)하세요 (brain.ts 96~109행):

blockStreamingChunk: {
  minChars: 1,
  maxChars: 200,
...

가장 중요한 줄은 flushOnEnqueue: true입니다. 이 설정은 coalescer에게 블록이 도착하는 즉시 기다리지 말고 코드로 보내라고 지시합니다. 다른 모든 오버라이드 설정도 필요하지만, 이 설정 없이는 무용지물입니다.

델타(delta) 직접 추적하기

콜백(callback)은 새로운 블록이 도착하는 것과 같은 어떤 사건이 발생했을 때 OpenClaw가 호출하는 함수입니다. OpenClaw의 onBlockReply 콜백에는 새로운 조각만이 아니라 지금까지의 전체 텍스트가 전달됩니다. 따라서 무엇이 새로운 부분인지 직접 파악해야 합니다. 이 새로운 조각을 델타(delta)라고 부릅니다.

리포지토리에서 이를 계산하는 방식은 다음과 같습니다 (brain.ts 486~501행):

let delta: string;
if (currentBlockStream && text.startsWith(currentBlockStream)) {
  delta = text.slice(currentBlockStream.length);
...

여기에는 세 가지 케이스가 있으며, 세 번째 케이스가 가장 중요합니다:

  1. Extension (확장). 새로운 텍스트가 기존 텍스트로 시작됩니다. 델타(Delta)는 단순히 끝부분에 있는 부분입니다. 간단합니다.
  2. Duplicate (중복). 동일한 블록이 두 번 보고되었습니다. 건너뜁니다.
  3. Reset (리셋). 새로운 텍스트가 기존 텍스트와 아무런 관련이 없습니다. 이는 도구 호출(Tool call)이 완료된 후에 발생합니다. OpenClaw는 새로운 블록 스트림(Block stream)을 시작하며, 새로운 텍스트는 완전히 새로운 문자열이 됩니다. 이 분기(Branch)가 없다면, 새로운 블록을 놓치거나 기존 블록에 잘못 결합하게 됩니다.

payload.text의 빈 페이로드 특이사항

블록 스트리밍(Block streaming)이 실제로 작동할 때, 최종 응답의 payload.text는 빈 문자열입니다. 이것은 버그가 아닙니다.

OpenClaw에는 shouldDropFinalPayloads라는 체크 로직이 있어, 텍스트가 이미 스트리밍된 경우 최종 페이로드에서 해당 텍스트를 제거합니다. 이는 동일한 텍스트를 두 번 보내는 것을 방지합니다. 리포지토리(Repo)는 청크(Chunk)가 도착함에 따라 자체 버퍼(canonicalText)에 텍스트를 수집함으로써 이를 처리합니다. 버퍼가 비어 있는 경우에만 payload.text를 대체 수단으로 사용합니다:

if (!canonicalText && payloadText) canonicalText = payloadText;

Murf Falcon

합성(Synthesis)은 텍스트로부터 오디오를 생성하는 기술적 용어입니다. Murf Falcon은 이 빌드에서 사용되는 TTS(Text-to-Speech) 모델입니다. Murf는 모델 지연 시간(Latency) 55ms, 첫 오디오 도달 시간(Time-to-first-audio) 130ms를 보고하며, 가격은 1,000자당 $0.01입니다. 이는 생성된 오디오 1분당 약 1센트 수준입니다.

OpenClaw의 내장 TTS 끄기

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0