npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@getvoicify/pipecat

v0.3.1

Published

Built on [Pipecat](https://github.com/pipecat-ai/pipecat).

Readme

Pipecat

Built on Pipecat.

An Angular-native wrapper around @pipecat-ai/client-js. It exposes the Pipecat client SDK as injectable Angular services backed by signals and observables — connection state, device state, and messaging as reactive state instead of a raw imperative client — while still giving full event parity with the underlying SDK via on().

Install

Install alongside the SDK it wraps and a transport implementation of your choice (e.g. Daily):

npm install @getvoicify/pipecat @pipecat-ai/client-js @pipecat-ai/daily-transport

@angular/core and @angular/common ^21.2.0 and @pipecat-ai/client-js ~1.13.0 are peer dependencies. The client-js range is a tilde, not a caret, because the official transports pin ~1.13.0 themselves — a caret here would let you resolve a client-js minor your transport does not support. The transport package (Daily, or any other Transport implementation supported by @pipecat-ai/client-js) is a separate install — this library has no dependency on any specific one.

Quick start

Provide the client in your app config with providePipecat(), and provide PIPECAT_TRANSPORT with a concrete Transport instance:

import { ApplicationConfig } from '@angular/core';
import { providePipecat, PIPECAT_TRANSPORT } from '@getvoicify/pipecat';
import { DailyTransport } from '@pipecat-ai/daily-transport';

export const appConfig: ApplicationConfig = {
  providers: [
    providePipecat({
      /* PipecatClientOptions, minus `transport` */
    }),
    { provide: PIPECAT_TRANSPORT, useFactory: () => new DailyTransport() },
  ],
};

Then inject Pipecat and connect:

import { Component, inject } from '@angular/core';
import { Pipecat } from '@getvoicify/pipecat';

@Component({ /* ... */ })
export class VoiceWidget {
  private readonly pipecat = inject(Pipecat);

  readonly state = this.pipecat.state;
  readonly error = this.pipecat.error;

  start(): void {
    this.pipecat.startBotAndConnect({ endpoint: '/api/start-bot' });
  }
}

state and error are signals derived from the client's own transport-state and error events, so templates can react to connection status directly without subscribing to anything manually.

Scoping it to a lazy route

providePipecat() supplies every service in this library, so it works equally well in a lazy route's providers array as at the application root. The laziness has to reach the file holding the transport import, though: the import { DailyTransport } statement is what pulls the vendor SDK in, so a providers array that sits in an eagerly-imported route config buys you nothing. app.config.ts calls provideRouter(routes), which makes app.routes.ts part of the initial chunk — and any static import at the top of it too, however lazy the loadComponent below is.

So keep the transport out of app.routes.ts and point it at a route file that is itself lazily loaded:

// app.routes.ts — eagerly imported by app.config.ts, so no transport here.
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'voice',
    loadChildren: () => import('./voice/voice.routes').then((m) => m.VOICE_ROUTES),
  },
];
// voice/voice.routes.ts — fetched only when someone visits /voice.
import { Routes } from '@angular/router';
import { providePipecat, PIPECAT_TRANSPORT } from '@getvoicify/pipecat';
import { DailyTransport } from '@pipecat-ai/daily-transport';

export const VOICE_ROUTES: Routes = [
  {
    path: '',
    providers: [
      providePipecat(),
      { provide: PIPECAT_TRANSPORT, useFactory: () => new DailyTransport() },
    ],
    loadComponent: () => import('./voice-page').then((m) => m.VoicePage),
  },
];

Measured on this repo's demo app with ng build (production) and the Daily transport:

| where the providers and the transport import live | initial bundle | | --- | --- | | no voice route at all (baseline) | 218.81 kB | | app.routes.ts (eagerly imported) | 616.77 kB | | voice.routes.ts (behind loadChildren) | 223.30 kB |

The middle row is the trap: it reads as lazy and is not. loadComponent duly defers the component — all 402 bytes of it — while the SDK it was meant to defer has already shipped in the initial chunk, overshooting Angular's default 500 kB initial budget by 116.77 kB. The same two providers behind loadChildren leave the initial bundle 4.49 kB above the no-voice baseline and move ~394 kB into chunks that are fetched only when someone visits /voice.

Both forms are supported; use the root one when the client should be available application-wide.

By default, providePipecat() disconnects the client automatically when its providing injector is destroyed, and { persistOnRoute: true } opts out of that. Neither applies to route-scoped usage. Angular does not destroy the environment injector it creates for a Route.providers array when you navigate away from that route: probed on Angular 21.2 with a real provideRouter, the route client's disconnect() was called 0 times after navigating to another route, 0 times after Router.resetConfig([]), and 0 times after the whole test environment was torn down — while destroying an environment injector directly called it once, so the teardown hook itself works. On a route, therefore, the automatic disconnect never fires and persistOnRoute has no effect: leaving the voice route keeps the microphone and the connection live. Disconnect explicitly from the component's own teardown instead.

import { Component, DestroyRef, inject } from '@angular/core';
import { Pipecat } from '@getvoicify/pipecat';

@Component({ /* ... */ })
export class VoicePage {
  private readonly pipecat = inject(Pipecat);

  constructor() {
    inject(DestroyRef).onDestroy(() => this.pipecat.disconnect());
  }
}

Using a transport other than Daily

PIPECAT_TRANSPORT is an InjectionToken<Transport>, and providePipecat() does nothing with the instance beyond passing it to PipecatClient. Any Transport the SDK supports drops into the slot Daily occupies in the examples above, and nothing else about the wiring changes. What differs between transports is the connection parameters each one expects.

SmallWebRTC is worth spelling out because it is the transport you reach for when the orchestrator is your own Pipecat server rather than a hosted room, and because one of its options is both easy to miss and quiet when it is missing.

npm install @pipecat-ai/small-webrtc-transport

@pipecat-ai/[email protected] declares "@pipecat-ai/client-js": "~1.13.0" as its only peer dependency — the same tilde range this library declares, for the reason given under Install. One client-js resolution satisfies both.

The provider is the route-scoped shape from the previous section with the class swapped:

// voice/voice.routes.ts — fetched only when someone visits /voice.
import { Routes } from '@angular/router';
import { providePipecat, PIPECAT_TRANSPORT } from '@getvoicify/pipecat';
import { SmallWebRTCTransport } from '@pipecat-ai/small-webrtc-transport';

export const VOICE_ROUTES: Routes = [
  {
    path: '',
    providers: [
      providePipecat(),
      {
        provide: PIPECAT_TRANSPORT,
        useFactory: () =>
          new SmallWebRTCTransport({
            offerUrlTemplate: '/api/sessions/:sessionId/offer',
          }),
      },
    ],
    loadComponent: () => import('./voice-page').then((m) => m.VoicePage),
  },
];

Everything the previous section says about lazy loading applies unchanged. The bundle figures there were measured with Daily and will differ for another transport, but the shape of the trap does not.

offerUrlTemplate: where the SDP offer is sent

SmallWebRTC connects in two requests, not one. startBotAndConnect() POSTs to your start endpoint to spawn the bot, then POSTs the WebRTC SDP offer to a second URL and expects an SDP answer back. offerUrlTemplate is how you say where that second URL is. Three things about it are worth knowing before you write the call.

It is a constructor option, not a connect parameter. It is declared on SmallWebRTCTransportConstructorOptions, not on the SmallWebRTCTransportConnectionOptions that connect() accepts, so it has to go to new SmallWebRTCTransport({ ... }) — inside the PIPECAT_TRANSPORT factory. There is no way to supply it later.

It interpolates the literal substring :sessionId. Not {sessionId}, not ${sessionId}, and not a path segment: the transport calls .replace(':sessionId', sessionId), so the placeholder is matched literally and only the first occurrence is substituted. The session id comes from the body your start endpoint returned — the transport reads a sessionId or session_id key off it (snake_case keys are camel-cased first) and strips it out of the connection parameters before passing the rest on.

It is only consulted on the startBot path. The template is read from one place: a fallback that runs only when the transport already knows a start endpoint, which it knows because startBot() recorded it before making the request. Spawn the bot with your own fetch() and then call Pipecat.connect(), and no start endpoint was ever recorded, the fallback is skipped, and offerUrlTemplate is dead configuration. Reach it via Pipecat.startBotAndConnect(params), or via Pipecat.startBot(params) followed by Pipecat.connect() with the response it resolved to. If you must spawn the bot yourself, pass webrtcRequestParams instead — see below.

The silent failure when offerUrlTemplate is absent

Without a template, the offer URL is derived from the start endpoint by string substitution:

// vendor internals — @pipecat-ai/small-webrtc-transport, not this library's API
const offerUrl = this.offerUrlTemplate
  ? this.offerUrlTemplate.replace(':sessionId', sessionId)
  : startEndpoint.replace('/start', `/sessions/${sessionId}/api/offer`);

String.prototype.replace with a string pattern returns its input unchanged when the pattern does not occur. A start endpoint with no /start in it — /api/bot/spawn, /api/connect, https://bot.example.com/v1/sessions — therefore yields an offer URL identical to the start endpoint, and the SDP offer is POSTed straight back to the bot-spawn endpoint. Nothing rejects it. That endpoint is real and working, so it answers 200 with its usual { sessionId, status } body, and the transport hands that to RTCPeerConnection.setRemoteDescription().

What you observe:

  • connect() neither throws nor resolves, and state stays on connecting.
  • The rejection from setRemoteDescription() is caught inside the transport and logged at debug level — invisible unless you have raised the SDK's log level — as Reconnection attempt 0 failed: ..., on a first connect, which reads as noise even when you do see it.
  • Three retries follow at two-second intervals, each POSTing the offer to the same wrong URL.
  • After the third, the transport stops and rejects the connect promise with no argument. This library stringifies whatever it caught, so Pipecat.error() ends up holding a message that is the literal string undefined.

No type error, no HTTP error, and an error signal that names nothing. The network tab is the fastest confirmation: two POSTs to the same URL, the second carrying an sdp field and getting a non-SDP body back.

A partial match misfires too, because the pattern is a substring rather than a path segment. /api/start-bot does contain /start, so it rewrites to /api/sessions/<id>/api/offer-bot — wrong, but wrong loudly, as a 404. Setting offerUrlTemplate explicitly rules out both cases.

webrtcRequestParams, and two deprecated options

connectionUrl and webrtcUrl are both marked @deprecated in the transport's typings, superseded by webrtcRequestParams; passing either logs <key> is deprecated. Use webrtcRequestParams instead., and supplying one alongside webrtcRequestParams logs a second warning and uses webrtcRequestParams. Build on webrtcRequestParams — an APIRequest, the same { endpoint, headers?, requestData?, timeout? } shape startBot() takes — and skip the other two.

It points the offer at a fixed URL rather than a per-session one, and unlike offerUrlTemplate it is honoured both on the constructor and on connect():

this.pipecat.connect({ webrtcRequestParams: { endpoint: '/api/offer' } });

One caveat, because the two options do not compose the way they look like they should: on the startBot path the derived offer URL is built whenever the start response carries a session id and itself carries no WebRTC parameters — and it then overrides a webrtcRequestParams given to the constructor. So a constructor webrtcRequestParams does not protect you from the trap above; only offerUrlTemplate does. The division that holds:

| how the bot is spawned | option to use | | --- | --- | | startBotAndConnect(), or startBot() then connect() | offerUrlTemplate | | your own fetch(), then connect() | webrtcRequestParams on connect() |

A start endpoint that returns webrtcRequestParams (or webrtc_request_params) in its own response body works too, and takes precedence over both — the server-driven option, if you would rather not encode the URL shape in the client at all.

Running the transport outside a bundler

The transport's ESM entry point imports lodash/cloneDeep without a file extension, and Node's ESM resolver does no extension guessing. Importing it under plain node therefore fails with ERR_MODULE_NOT_FOUND, and a hint suggesting lodash/cloneDeep.js instead (checked on Node 22.19). esbuild — which Angular's application builder uses — resolves it, and so does Bun, so this never surfaces in an application. It surfaces in a bare-Node script written against the transport directly, which is exactly where you would go to reproduce a connection problem outside the browser. Run that script under Bun, or bundle it first.

Service surface

Pipecat

The main facade, covering connection lifecycle:

  • connect(params?) / disconnect()
  • startBot(params) — starts a bot without connecting a transport, returns the SDK's response promise
  • startBotAndConnect(params) — starts a bot and connects in one call
  • disconnectBot()
  • state / error — signals derived from TransportStateChanged and Error events (and from rejected promises on the calls above)
  • on(event) — full event parity, see below
  • .devices, .messaging, .functions, .uiCommands, .jobGroups, .conversation — the sibling services below, aggregated for convenience

Pipecat.devices (PipecatDevices)

Microphone/camera/speaker/screen-share state and control: signals for mediaState, selectedMic, selectedCam, selectedSpeaker, plus methods to enumerate devices (getAllMics/getAllCams/getAllSpeakers), select or toggle them (updateMic/updateCam/updateSpeaker, enableMic/enableCam/enableScreenShare), and query current state (isMicEnabled, isCamEnabled, isSharingScreen, tracks, needsInit, initDevices).

Two more signal pairs worth calling out:

  • micEnabled, camEnabled, sharingScreen — readonly signals mirroring isMicEnabled()/isCamEnabled()/isSharingScreen(), but reactive: they update whenever enableMic()/enableCam()/enableScreenShare() (or the toggle directives below, which call the same methods) change the corresponding state, so templates can bind to them directly instead of polling the method form.
  • liveTracks — a signal snapshot of tracks() that re-emits on every track-lifecycle event (mic/cam/screen-share started or stopped, for both the local participant and the bot). This is what PipecatAudio and PipecatVideo (below) bind to internally.

Pipecat.messaging (PipecatMessaging)

Sending data to and from the bot: sendClientMessage, sendUIEvent, sendDTMF, sendClientRequest, sendText, appendToContext, plus the UI snapshot streaming controls (startUISnapshotStream, stopUISnapshotStream, cancelUIJobGroup).

Pipecat.functions (PipecatFunctions)

Registering handlers the bot can invoke as function/tool calls: registerFunctionCallHandler, unregisterFunctionCallHandler, unregisterAllFunctionCallHandlers.

Pipecat.conversation (PipecatConversation)

Builds a text transcript from the bot's transcription/LLM-output event stream:

  • turns — a signal of committed { role: 'user' | 'bot', text, timestamp } entries, in chronological order.
  • currentUserPartial / currentBotPartial — live in-flight text: interim speech-to-text while the user is talking, and the bot's response as it streams in, before either commits to turns.
  • clear() — resets everything.

A user turn commits when the transcription event is marked final; a bot turn commits when the bot's LLM finishes generating (an LLM-stop with no accumulated text produces no phantom empty turn). This is scoped to text content only — it does not track speaking-activity events (no "user started/stopped speaking" indicators).

@for (turn of pipecat.conversation.turns(); track turn.timestamp) {
  <p [class]="turn.role">{{ turn.text }}</p>
}
@if (pipecat.conversation.currentUserPartial(); as partial) {
  <p class="user pending">{{ partial }}</p>
}
@if (pipecat.conversation.currentBotPartial(); as partial) {
  <p class="bot pending">{{ partial }}</p>
}

Pipecat.uiCommands (PipecatUICommands)

Dispatches inbound ui-command bot messages to handlers registered by name: registerCommandHandler(command, handler) / unregisterCommandHandler(command) — the same idiom as PipecatFunctions.registerFunctionCallHandler. A command with no registered handler warns instead of throwing, and a handler that throws doesn't break dispatch for subsequent commands.

Pipecat.jobGroups (PipecatUIJobGroups)

Aggregates the bot's ui-job-group event stream — a multi-worker async job lifecycle: started → per-worker updates → per-worker completed → group completed — into a jobGroups signal keyed by job id, with per-worker status/latestUpdate/response. Completed groups stay in state until the app explicitly clears them via clearJobGroup(jobId) / clearCompletedJobGroups() — they are not auto-removed. Pipecat.messaging.cancelUIJobGroup(jobId, reason?) is the way to ask the server to cancel one.

DOM components

PipecatAudio

Renders the bot's audio track (if any) into an <audio autoplay> element via srcObject. It has no inputs — drop it in wherever you want bot audio to play:

<gvo-pipecat-audio />

PipecatVideo

Renders a participant's video track (if any) into a <video autoplay> element via srcObject. The participantType input selects whose video: 'local' for the local participant, 'bot' for the bot.

<gvo-pipecat-video participantType="local" />
<gvo-pipecat-video participantType="bot" />

Both components read from Pipecat.devices' live track state, so a track starting or ending — mic, camera, or screen-share, for either participant — is reflected automatically without any manual wiring.

Toggle directives

PipecatMicToggle (gvoPipecatMicToggle), PipecatCamToggle (gvoPipecatCamToggle), and PipecatScreenShareToggle (gvoPipecatScreenShareToggle) are attribute directives, not components — apply one to your own element (typically a <button>) and it imposes no styling of its own. Each toggles the corresponding device on click and exposes its live enabled signal through the directive's exportAs name, readable via a template-reference variable:

<button gvoPipecatMicToggle #t="gvoPipecatMicToggle">
  {{ t.enabled() ? 'Mute' : 'Unmute' }}
</button>

<button gvoPipecatCamToggle #c="gvoPipecatCamToggle">
  {{ c.enabled() ? 'Stop video' : 'Start video' }}
</button>

<button gvoPipecatScreenShareToggle #s="gvoPipecatScreenShareToggle">
  {{ s.enabled() ? 'Stop sharing' : 'Share screen' }}
</button>

UI Worker Protocol

registerDefaultUICommandHandlers(uiCommands) registers the three standard built-in commands the protocol documents — click, set_input_value, select_text — letting the bot click elements, write into form fields, and change text selection on the page. This is real DOM-mutating behavior, which is exactly why it's opt-in rather than automatic: nothing in the library calls it for you.

It resolves target elements via ref (a snapshot-assigned reference id, from the SDK's accessibility-snapshot system) or target_id (a plain DOM element id), trying ref first. It refuses to write into disabled/readonly/ hidden inputs. It's SSR-safe: it no-ops on the server, and never throws.

import { inject } from '@angular/core';
import { PipecatUICommands, registerDefaultUICommandHandlers } from '@getvoicify/pipecat';

// once, e.g. in a root component or app initializer
registerDefaultUICommandHandlers(inject(PipecatUICommands));

// registering your own custom command, alongside the built-ins
inject(PipecatUICommands).registerCommandHandler('open_settings_dialog', (payload) => {
  // ...
});

Job-group state — multi-step async worker jobs the bot dispatches — is tracked separately via Pipecat.jobGroups, documented above in the service surface section.

PipecatVoiceVisualizer

Renders a bar-style visualization of a participant's audio level onto a <canvas>. The participantType input selects whether the bars are driven by the local participant's or the bot's audio level.

<gvo-pipecat-voice-visualizer participantType="bot" />

Optional inputs tune the bars: barCount (default 5), barGap (12), barWidth (30), barMaxHeight (120), barLineCap ('round' | 'square', default 'round'), and barOrigin ('bottom' | 'center' | 'top', default 'center').

Theming

Bar and background color aren't hardcoded — they resolve through CSS custom properties, so the visualizer picks up your design tokens with zero component configuration:

gvo-pipecat-voice-visualizer {
  --gvo-pipecat-visualizer-bar-color: #6366f1;
  --gvo-pipecat-visualizer-background-color: #1e1b4b;
}

If a custom property isn't set, the bar color falls back to the element's computed color (so it inherits ambient text color for free) and the background stays transparent. For the rare case a CSS custom property isn't practical — e.g. a color computed at runtime — the barColor and backgroundColor inputs take precedence over the CSS custom property whenever they're provided.

SSR

On the server platform this library is inert. providePipecat() resolves a no-op stand-in client: no transport is constructed (your transport factory is never invoked), no PipecatClient is constructed, no events ever fire, and every signal stays at its initial value. Server-rendered markup therefore shows the disconnected/initial state, and the real PipecatClient — backed by your PIPECAT_TRANSPORT — is constructed on the browser platform only. It is built fresh once the application hydrates and carries no state across from the server render: the application hydrates, the client does not.

Both halves of that guard are load-bearing. Your transport (Daily, WebSocket, or any other Transport implementation) touches browser-only APIs like WebRTC and navigator.mediaDevices just by being instantiated; the SDK's PipecatClient constructor separately evaluates a bare window reference, which throws ReferenceError under Node rather than resolving to undefined.

One consequence is worth knowing: on the server the lifecycle calls resolve without doing anything. connect() and startBotAndConnect() resolve a placeholder BotReadyData of { version: '' } and startBot() resolves undefined, having connected nothing. They resolve rather than reject because Pipecat.connect() and startBotAndConnect() are fire-and-forget — they only attach a .catch() to a promise they never hand back — so a server-side rejection would push a spurious error into error on every render, and any consumer calling the client directly without a handler would raise an unhandled rejection. Code that awaits these calls must therefore not treat resolution as proof of a connection; read state instead.

You don't need to guard providePipecat() or your transport factory against the server platform yourself.

Full event parity: on() / fromClientEvent()

The services above curate the events that map naturally to signals, but the underlying SDK emits a much larger set of RTVIEvents. Pipecat.on(event) mirrors the client's own client.on(event, callback) as an Observable, so any RTVIEvent is reachable reactively even if it isn't exposed as a dedicated signal:

this.pipecat.on(RTVIEvent.BotTranscript).subscribe((data) => {
  /* ... */
});

This is built on the standalone fromClientEvent(client, event) helper (projects/pipecat/src/lib/events.ts), which is fully typed per-event via the SDK's own RTVIEventHandler map — no manual type parameters needed at call sites. Note it only captures the first argument of multi-argument event handlers.

Caveat: HttpClient / interceptor bypass

connect() (when passed the deprecated ConnectionEndpoint-shaped param, not the common TransportConnectionParams case), startBot(), and startBotAndConnect() all end up calling the SDK's internal request helper, which issues a raw fetch() call directly rather than going through Angular's HttpClient. That means registered HttpInterceptors — auth header injection, XSRF protection, retry, logging — never run for these specific requests. If your endpoint needs auth or other headers an interceptor would normally add, attach them manually via the call's headers param.

Testing

To execute unit tests, run:

ng test pipecat

Releasing

Releases are automated with release-please. Commits on main must follow Conventional Commits (feat:, fix:, chore:, docs:, etc.); a breaking change is feat!:/fix!: or a BREAKING CHANGE: footer. release-please keeps an open "release PR" that accumulates the pending version bump and changelog for every conventional commit since the last release — merging that PR cuts the release (tags v{version} and updates CHANGELOG.md), which triggers the existing publish.yml workflow to build and publish to npm automatically. No manual tagging or npm publish is needed.

License

BSD-2-Clause