@pipedog/serializer-protobuf 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024–2026, Daily
4
+ Copyright (c) 2026, pipedog contributors
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ pipedog is an unofficial TypeScript port of Pipecat (https://github.com/pipecat-ai/pipecat),
2
+ a framework for voice and multimodal conversational AI created by Daily.
3
+
4
+ It is not affiliated with or endorsed by Daily. Pipecat is distributed under
5
+ the BSD 2-Clause License, Copyright (c) 2024–2026, Daily; that copyright notice
6
+ and license are retained in LICENSE, and every ported source file carries a
7
+ header naming the upstream file it was derived from.
8
+
9
+ The upstream commit each file was ported from is recorded in porting/manifest.json.
package/README.md CHANGED
@@ -1,3 +1,61 @@
1
- # Temporary Holding Version
1
+ # @pipedog/serializer-protobuf
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `ProtobufFrameSerializer` for pipedog, a port of Pipecat's `pipecat.serializers.protobuf`. It
4
+ speaks Pipecat's `frames.proto` over WebSocket: text, audio, transcription, interruption and
5
+ transport messages, the last as a `MessageFrame` holding JSON. It is the default serializer of
6
+ the WebSocket transports, and the format the official Pipecat browser client uses.
7
+
8
+ ```sh
9
+ bun add @pipedog/serializer-protobuf
10
+ ```
11
+
12
+ ```ts
13
+ import { ProtobufFrameSerializer } from "@pipedog/serializer-protobuf/protobuf";
14
+
15
+ const serializer = new ProtobufFrameSerializer();
16
+ const bytes = await serializer.serialize(frame); // Uint8Array, or null for other frame types
17
+ const decoded = await serializer.deserialize(bytes); // InputAudioRawFrame, InputTransportMessageFrame, ...
18
+ ```
19
+
20
+ Pass it as `serializer` to any transport in
21
+ [`@pipedog/transport-websocket`](../transport-websocket).
22
+
23
+ ## Runtimes
24
+
25
+ | class | Bun | Node | Deno | Cloudflare Workers |
26
+ |---|:-:|:-:|:-:|:-:|
27
+ | `ProtobufFrameSerializer` | ✅ | ✅ | ✅ | ✅ |
28
+
29
+ The codec is core's `frames/protobufs/frames_pb2`: a hand-written proto3 codec for the same
30
+ schema, with no dependencies. `compat/smoke.ts` replays the golden vectors on all four runtimes.
31
+
32
+ ## Wire compatibility
33
+
34
+ Its wire format is the protocol, so it is checked byte for byte:
35
+
36
+ - **Upstream** (`test/golden.test.ts`, vectors recorded from upstream Pipecat by
37
+ `tools/golden/serializers.py`). Serializing gives identical bytes for every frame type: unicode
38
+ text, empty and large ids, mono and stereo audio, with and without `pts`, transcriptions, and
39
+ interruptions. Transport messages with nested, unicode, emoji, control-character, float and
40
+ big-int data also match, down to the JSON text, which is written as Python's `json.dumps`
41
+ writes it. Deserializing upstream's bytes, unknown fields included, gives the same frames.
42
+ - **The official browser client**, `@pipecat-ai/websocket-transport` 1.7.2
43
+ (`test/client_js_compat.test.ts`, with fixtures from `tools/client-compat/`). Our serializer
44
+ decodes the bytes the client sends: mic audio and RTVI messages. The client decodes ours:
45
+ audio and RTVI messages. The client's `frames.proto` predates `InterruptionFrame`, so it logs
46
+ and ignores interruption, text and transcription frames, from upstream's bots too. A real
47
+ `PipecatClient` was also run headless against `SingleClientWebsocketServerTransport` with
48
+ `tools/client-compat/e2e.ts`.
49
+
50
+ Differences from upstream:
51
+
52
+ - `uint64` ids are JS numbers, exact up to 2^53.
53
+ - Transport messages are parsed with `JSON.parse`, which rejects the `NaN` and `Infinity` that
54
+ Python's `json` accepts. Such a message is dropped with an error log.
55
+ - A string field with invalid UTF-8 is decoded with replacement characters, where upstream's
56
+ protobuf raises `DecodeError`.
57
+ - JavaScript has no separate int and float types, so `2.0` arrives as `2`.
58
+
59
+ ## License
60
+
61
+ BSD 2-Clause, a port of Pipecat (Copyright (c) 2024–2026 Daily).
@@ -0,0 +1,3 @@
1
+ /** Protobuf frame serializer. `@pipedog/serializer-protobuf/protobuf` mirrors `pipecat.serializers.protobuf`. */
2
+ export * from "./protobuf.ts";
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026, pipedog contributors
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /** Protobuf frame serializer. `@pipedog/serializer-protobuf/protobuf` mirrors `pipecat.serializers.protobuf`. */
4
+ export * from "./protobuf.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,54 @@
1
+ import { type Frame } from "@pipedog/core/frames/frames";
2
+ import * as frame_protos from "@pipedog/core/frames/protobufs/frames_pb2";
3
+ import { FrameSerializer, type FrameSerializerOptions, type SerializedData } from "@pipedog/core/serializers/base_serializer";
4
+ export interface MessageFrameOptions {
5
+ /** JSON-encoded message data for transport. */
6
+ data: string;
7
+ }
8
+ /**
9
+ * Data class for converting transport messages into Protobuf format.
10
+ *
11
+ * - `data`: JSON-encoded message data for transport.
12
+ */
13
+ export declare class MessageFrame {
14
+ data: string;
15
+ constructor(options: MessageFrameOptions);
16
+ toString(): string;
17
+ }
18
+ type AnyCtor = abstract new (...args: any[]) => unknown;
19
+ type ConcreteCtor = new (options: any) => Frame | MessageFrame;
20
+ /**
21
+ * Serializer for converting Pipecat frames to/from Protocol Buffer format.
22
+ *
23
+ * Provides efficient binary serialization for frame transport over network
24
+ * connections. Supports text, audio, transcription, and message frames with
25
+ * automatic conversion between transport message types.
26
+ */
27
+ export declare class ProtobufFrameSerializer extends FrameSerializer {
28
+ static readonly SERIALIZABLE_TYPES: ReadonlyMap<AnyCtor, frame_protos.FrameOneofName>;
29
+ static readonly SERIALIZABLE_FIELDS: ReadonlyMap<frame_protos.FrameOneofName, AnyCtor>;
30
+ static readonly DESERIALIZABLE_TYPES: ReadonlyMap<ConcreteCtor, frame_protos.FrameOneofName>;
31
+ static readonly DESERIALIZABLE_FIELDS: ReadonlyMap<string, ConcreteCtor>;
32
+ /**
33
+ * Initialize the Protobuf frame serializer.
34
+ *
35
+ * @param options.params Configuration parameters.
36
+ */
37
+ constructor(options?: FrameSerializerOptions);
38
+ /**
39
+ * Serialize a frame to Protocol Buffer binary format.
40
+ *
41
+ * @param frame The frame to serialize.
42
+ * @returns Serialized frame as bytes, or null if frame type is not serializable.
43
+ */
44
+ serialize(frame: Frame): Promise<SerializedData | null>;
45
+ /**
46
+ * Deserialize Protocol Buffer binary data to a frame.
47
+ *
48
+ * @param data Binary protobuf data to deserialize.
49
+ * @returns Deserialized frame instance, or null if deserialization fails.
50
+ */
51
+ deserialize(data: SerializedData): Promise<Frame | null>;
52
+ }
53
+ export {};
54
+ //# sourceMappingURL=protobuf.d.ts.map
@@ -0,0 +1,192 @@
1
+ // Copyright (c) 2024-2026, Daily
2
+ // Copyright (c) 2026, pipedog contributors
3
+ // SPDX-License-Identifier: BSD-2-Clause
4
+ //
5
+ // Ported from pipecat: src/pipecat/serializers/protobuf.py
6
+ /** Protobuf frame serialization for Pipecat. */
7
+ import { rethrowIfCancelled } from "@pipedog/asyncio";
8
+ import { InputAudioRawFrame, InputTransportMessageFrame, InterruptionFrame, OutputAudioRawFrame, OutputTransportMessageFrame, OutputTransportMessageUrgentFrame, TextFrame, TranscriptionFrame, } from "@pipedog/core/frames/frames";
9
+ import * as frame_protos from "@pipedog/core/frames/protobufs/frames_pb2";
10
+ import { FrameSerializer, } from "@pipedog/core/serializers/base_serializer";
11
+ import { logger } from "@pipedog/core/utils/logger";
12
+ import { pyJsonDumps as jsonDumps, pyJsonLoads } from "@pipedog/core/utils/py_json";
13
+ /**
14
+ * Data class for converting transport messages into Protobuf format.
15
+ *
16
+ * - `data`: JSON-encoded message data for transport.
17
+ */
18
+ export class MessageFrame {
19
+ data;
20
+ constructor(options) {
21
+ this.data = options.data;
22
+ }
23
+ toString() {
24
+ return `MessageFrame(data=${JSON.stringify(this.data)})`;
25
+ }
26
+ }
27
+ /** Python's truthiness, for the values a frame field can hold. */
28
+ function isTruthy(value) {
29
+ if (value instanceof Uint8Array) {
30
+ return value.length > 0;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ return value.length > 0;
34
+ }
35
+ return Boolean(value);
36
+ }
37
+ /**
38
+ * Serializer for converting Pipecat frames to/from Protocol Buffer format.
39
+ *
40
+ * Provides efficient binary serialization for frame transport over network
41
+ * connections. Supports text, audio, transcription, and message frames with
42
+ * automatic conversion between transport message types.
43
+ */
44
+ export class ProtobufFrameSerializer extends FrameSerializer {
45
+ static SERIALIZABLE_TYPES = new Map([
46
+ [TextFrame, "text"],
47
+ [OutputAudioRawFrame, "audio"],
48
+ [TranscriptionFrame, "transcription"],
49
+ [MessageFrame, "message"],
50
+ [InterruptionFrame, "interruption"],
51
+ ]);
52
+ static SERIALIZABLE_FIELDS = new Map([...ProtobufFrameSerializer.SERIALIZABLE_TYPES].map(([k, v]) => [v, k]));
53
+ static DESERIALIZABLE_TYPES = new Map([
54
+ [TextFrame, "text"],
55
+ [InputAudioRawFrame, "audio"],
56
+ [TranscriptionFrame, "transcription"],
57
+ [MessageFrame, "message"],
58
+ [InterruptionFrame, "interruption"],
59
+ ]);
60
+ static DESERIALIZABLE_FIELDS = new Map([...ProtobufFrameSerializer.DESERIALIZABLE_TYPES].map(([k, v]) => [v, k]));
61
+ /**
62
+ * Initialize the Protobuf frame serializer.
63
+ *
64
+ * @param options.params Configuration parameters.
65
+ */
66
+ constructor(options = {}) {
67
+ super(options);
68
+ // The base serializer defaults to filtering out RTVI protocol messages
69
+ // to avoid sending them over telephony media streams. ProtobufFrameSerializer
70
+ // is used by WebSocket transports, which are the delivery channel for
71
+ // these messages, so we disable the filter.
72
+ this._params.ignoreRtviMessages = false;
73
+ }
74
+ /**
75
+ * Serialize a frame to Protocol Buffer binary format.
76
+ *
77
+ * @param frame The frame to serialize.
78
+ * @returns Serialized frame as bytes, or null if frame type is not serializable.
79
+ */
80
+ async serialize(frame) {
81
+ // Wrapping this messages as a JSONFrame to send
82
+ let serializable = frame;
83
+ if (frame instanceof OutputTransportMessageFrame ||
84
+ frame instanceof OutputTransportMessageUrgentFrame) {
85
+ if (this.shouldIgnoreFrame(frame)) {
86
+ return null;
87
+ }
88
+ serializable = new MessageFrame({
89
+ data: jsonDumps(frame.message),
90
+ });
91
+ }
92
+ const protoFrame = new frame_protos.Frame();
93
+ const ctor = serializable.constructor;
94
+ const protoOptionalName = ProtobufFrameSerializer.SERIALIZABLE_TYPES.get(ctor);
95
+ if (protoOptionalName === undefined) {
96
+ logger.warning(`Frame type ${ctor.name} is not serializable`);
97
+ return null;
98
+ }
99
+ // As protobuf's `getattr(proto_frame, name)`: the submessage only becomes
100
+ // the oneof's member once one of its fields is set.
101
+ const protoField = frame_protos.Frame.DESCRIPTOR.fieldsByName[protoOptionalName];
102
+ const ProtoType = PROTO_TYPES[protoOptionalName];
103
+ // Casts: Python sets and reads fields by name with getattr/setattr, genuinely dynamic.
104
+ const protoAttr = new ProtoType();
105
+ const source = serializable;
106
+ let anySet = false;
107
+ for (const field of protoField?.messageType?.fields ?? []) {
108
+ if (!(field.attr in source)) {
109
+ continue;
110
+ }
111
+ const value = source[field.attr];
112
+ if (isTruthy(value)) {
113
+ protoAttr[field.attr] = value;
114
+ anySet = true;
115
+ }
116
+ }
117
+ if (anySet) {
118
+ protoFrame[protoOptionalName] = protoAttr;
119
+ }
120
+ return protoFrame.serializeToString();
121
+ }
122
+ /**
123
+ * Deserialize Protocol Buffer binary data to a frame.
124
+ *
125
+ * @param data Binary protobuf data to deserialize.
126
+ * @returns Deserialized frame instance, or null if deserialization fails.
127
+ */
128
+ async deserialize(data) {
129
+ if (typeof data === "string") {
130
+ // As protobuf's `FromString`, which only accepts bytes.
131
+ throw new TypeError("a bytes-like object is required, not 'str'");
132
+ }
133
+ const proto = frame_protos.Frame.fromString(data);
134
+ const which = proto.whichOneof("frame");
135
+ const className = which === null ? undefined : ProtobufFrameSerializer.DESERIALIZABLE_FIELDS.get(which);
136
+ if (which === null || className === undefined) {
137
+ logger.error("Unable to deserialize a valid frame");
138
+ return null;
139
+ }
140
+ // Cast: Python reads the oneof member via getattr(proto, which), genuinely dynamic.
141
+ const args = proto[which];
142
+ const argsDict = {};
143
+ for (const field of frame_protos.Frame.DESCRIPTOR.fieldsByName[which]?.messageType?.fields ??
144
+ []) {
145
+ argsDict[field.attr] = args[field.attr];
146
+ }
147
+ // Remove special fields if needed
148
+ const id = args.id;
149
+ const name = args.name;
150
+ const pts = args.pts;
151
+ delete argsDict.id;
152
+ delete argsDict.name;
153
+ delete argsDict.pts;
154
+ let instance;
155
+ // Special handling for MessageFrame -> InputTransportMessageFrame
156
+ if (className === MessageFrame) {
157
+ try {
158
+ const msg = pyJsonLoads(argsDict.data);
159
+ instance = new InputTransportMessageFrame({ message: msg });
160
+ logger.debug(`ProtobufFrameSerializer: Transport message ${instance}`);
161
+ }
162
+ catch (e) {
163
+ rethrowIfCancelled(e);
164
+ logger.error(`Error parsing MessageFrame data: ${e instanceof Error ? e.message : e}`);
165
+ return null;
166
+ }
167
+ }
168
+ else {
169
+ // Normal deserialization, create the instance
170
+ instance = new className(argsDict);
171
+ }
172
+ // Set special fields
173
+ if (id) {
174
+ instance.id = id;
175
+ }
176
+ if (name) {
177
+ instance.name = name;
178
+ }
179
+ if (pts) {
180
+ instance.pts = pts;
181
+ }
182
+ return instance;
183
+ }
184
+ }
185
+ const PROTO_TYPES = {
186
+ text: frame_protos.TextFrame,
187
+ audio: frame_protos.AudioRawFrame,
188
+ transcription: frame_protos.TranscriptionFrame,
189
+ message: frame_protos.MessageFrame,
190
+ interruption: frame_protos.InterruptionFrame,
191
+ };
192
+ //# sourceMappingURL=protobuf.js.map
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@pipedog/serializer-protobuf",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Protobuf frame serializer (ProtobufFrameSerializer) for pipedog, wire-compatible with Pipecat's frames.proto.",
5
+ "license": "BSD-2-Clause",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/polvallverdu/pipedog.git",
9
+ "directory": "packages/serializer-protobuf"
10
+ },
11
+ "homepage": "https://github.com/polvallverdu/pipedog/tree/main/packages/serializer-protobuf#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/polvallverdu/pipedog/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./*": {
23
+ "types": "./dist/*.d.ts",
24
+ "default": "./dist/*.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "LICENSE",
30
+ "NOTICE"
31
+ ],
32
+ "dependencies": {
33
+ "@pipedog/asyncio": "0.3.0",
34
+ "@pipedog/core": "0.3.0"
35
+ },
36
+ "pipecat": {
37
+ "runtimes": [
38
+ "bun",
39
+ "node",
40
+ "deno",
41
+ "workerd"
42
+ ],
43
+ "upstream": [
44
+ "src/pipecat/serializers/protobuf.py"
45
+ ]
46
+ },
47
+ "main": "./dist/index.js",
48
+ "types": "./dist/index.d.ts"
49
+ }