HeyGen
HeyGen video avatars (LiveAvatar and Interactive Avatar) with HeyGenVideoService and HeyGenTransport.
Overview
HeyGen renders a talking avatar from your bot's speech. The session is controlled over HTTP and a WebSocket, and the avatar's video and audio travel through a LiveKit room. There are two ways to use it:
HeyGenVideoService(@pipedog/heygen): a pipeline service placed after your TTS. It sends the TTS audio to the avatar and pushes the avatar's video (OutputImageRawFrame) and audio (SpeechOutputAudioRawFrame) downstream, so any output transport can deliver them to the user.HeyGenTransport(@pipedog/transport-heygen): the bot joins the avatar's LiveKit room as a participant. The user joins the same room and sees the avatar directly; the bot hears the user and sends its speech to the avatar.
Both talk to LiveAvatar (ServiceType.LIVE_AVATAR, api.liveavatar.com) or to HeyGen's older
Interactive Avatar streaming API (ServiceType.INTERACTIVE_AVATAR, api.heygen.com, the
default for backwards compatibility).
Upstream page
Pipecat's documentation for the same service.
Source
video.ts, client.ts, api_liveavatar.ts and api_interactive_avatar.ts, plus
packages/transport-heygen/src/transport.ts.
LiveAvatar docs
Sessions, avatars and API keys.
Runtimes
| Bun | Node ≥ 22 | Deno | Cloudflare Workers | |
|---|---|---|---|---|
LiveAvatarApi, HeyGenApi (session HTTP APIs) | ✅ | ✅ | ✅ | not verified |
HeyGenClient control WebSocket | ✅ | ✅ | ✅ | not verified |
HeyGenVideoService, HeyGenTransport (avatar media) | ✅ | ✅ | ✅ (--allow-all) | ❌ |
The session APIs use only fetch and the control channel only WebSocket, so they have no
runtime requirements of their own, but the packages aren't tested on Workers. The media goes
through LiveKit's native Node SDK, @livekit/rtc-node, which loads a native N-API addon. Workers
can't load native code, so the service and the transport don't run there. Importing the packages
works everywhere: the addon is only loaded when the client sets up.
Installation
bun add @pipedog/heygen @livekit/rtc-node
# and, for the transport:
bun add @pipedog/transport-heygenimport { HeyGenVideoService } from "@pipedog/heygen/video";
import { HeyGenTransport, HeyGenParams } from "@pipedog/transport-heygen/transport";
import { LiveAvatarNewSessionRequest } from "@pipedog/heygen/api_liveavatar";
import { ServiceType } from "@pipedog/heygen/client";@livekit/rtc-node is an optional peer dependency: install it yourself (version 1.1.0 or newer in
the 1.x line). It ships prebuilt binaries for these platforms only:
| OS | Architectures |
|---|---|
| macOS | arm64 (darwin-arm64), x64 (darwin-x64) |
| Linux with glibc | x64 (linux-x64-gnu), arm64 (linux-arm64-gnu) |
| Windows | x64 (win32-x64-msvc) |
There is no build for musl (Alpine Linux): use a Debian- or Ubuntu-based image. Without the package, or on another platform, joining the room fails with an error that says how to install it.
Prerequisites
- A LiveAvatar account and its API key (for
ServiceType.LIVE_AVATAR; see LiveAvatar's docs), or a HeyGen account and API key for the Interactive Avatar API. Pass it asapiKey; the samples readHEYGEN_LIVE_AVATAR_API_KEY. - An avatar ID (
avatarId). Without a session request, the client uses upstream's default public avatar for the chosen API. - No LiveKit server of your own: HeyGen hosts the room and returns its URL and tokens. To use your
own LiveKit server or LiveKit Cloud instead, pass a
CustomSDKLiveKitConfigaslivekitConfig. - A long-running Bun, Node or Deno process on one of the platforms above.
HeyGenVideoService
Configuration
Prop
Type
The LiveAvatar session request:
Prop
Type
Usage
import { Pipeline } from "@pipedog/core/pipeline/pipeline";
import { PipelineParams, PipelineWorker } from "@pipedog/core/pipeline/worker";
import {
LiveAvatarNewSessionRequest,
VideoEncoding,
VideoSettings,
} from "@pipedog/heygen/api_liveavatar";
import { ServiceType } from "@pipedog/heygen/client";
import { HeyGenVideoService } from "@pipedog/heygen/video";
const heygen = new HeyGenVideoService({
apiKey: process.env.HEYGEN_LIVE_AVATAR_API_KEY ?? "",
serviceType: ServiceType.LIVE_AVATAR,
sessionRequest: new LiveAvatarNewSessionRequest({
avatarId: "<your LiveAvatar avatar id>",
videoSettings: new VideoSettings({ encoding: VideoEncoding.H264 }),
}),
});
// The avatar speaks the TTS audio; its video and audio come back as frames that
// the output transport sends to the user.
const worker = new PipelineWorker(
new Pipeline([
transport.input(),
stt,
userAggregator,
llm,
tts,
heygen,
transport.output(),
assistantAggregator,
]),
{ params: new PipelineParams({ audioInSampleRate: 16000, audioOutSampleRate: 24000 }) },
);
Notes
- TTS audio is resampled to 24 kHz and sent as
agent.speakmessages: a 400 ms first chunk, then 1 s chunks. When no audio arrives for 0.35 s, the rest is flushed andagent.speak_endsent. - A
UserStartedSpeakingFrameinterrupts the avatar (agent.interrupt) and starts its listening animation;UserStoppedSpeakingFramestops it. - The service joins the room with the user's token (
connectAsUser), so HeyGen doesn't end the session after five minutes without a user.
HeyGenTransport
Configuration
Prop
Type
HeyGenParams is a TransportParams with audioInEnabled and audioOutEnabled on by default.
Events
| Event | Arguments | When |
|---|---|---|
on_connected | (transport) | The bot joined the avatar's room. |
on_client_connected | (transport, participant) | A participant other than the avatar joined. Their audio is captured. |
on_client_disconnected | (transport, participant) | That participant left. |
Usage
import { HeyGenParams, HeyGenTransport } from "@pipedog/transport-heygen/transport";
const heygenTransport = new HeyGenTransport({
apiKey: process.env.HEYGEN_LIVE_AVATAR_API_KEY ?? "",
serviceType: ServiceType.LIVE_AVATAR,
sessionRequest: new LiveAvatarNewSessionRequest({ avatarId: "<your avatar id>" }),
params: new HeyGenParams({ audioInEnabled: true, audioOutEnabled: true }),
});
heygenTransport.eventHandler("on_client_connected", async (_transport, participant) => {
console.log(`user joined the avatar's room: ${participant}`);
});
const avatarWorker = new PipelineWorker(
new Pipeline([
heygenTransport.input(),
stt,
userAggregator,
llm,
tts,
heygenTransport.output(),
assistantAggregator,
]),
);
The session's room link (https://meet.livekit.io/custom?...) is logged at info level when the
session starts; open it to join as the user.
Runtime notes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ❌ no | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/heygen/package.json: ✅ means the compat smoke test passes on that runtime.
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ❌ no | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/transport-heygen/package.json: ✅ means the compat smoke test passes on that runtime.
- The session APIs use
fetch; passfetchto replace the global one (upstream takes an aiohttp session). - The control WebSocket uses the global
WebSocket.clientOptions.websocketOptionstakes awebSocketFactoryfor runtimes that open sockets differently. - LiveAvatar's API documents
livekit_agent_tokenandws_urlas optional. Upstream requires them; here a session without a WebSocket URL runs without avatar control (logged as a warning), and one without an agent token joins the room with the client token. - Callbacks given to
HeyGenClient.captureParticipantAudioandcaptureParticipantVideoreceive the frame fields (audio,sampleRate,numChannels/image,size,format) rather than anAudioRawFrameorImageRawFrameinstance.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).