@inferencesh/app 0.1.13 → 0.1.15
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/README.md +41 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/socket.d.ts +4 -0
- package/dist/stream.d.ts +17 -0
- package/dist/stream.js +45 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -256,6 +256,47 @@ export class App {
|
|
|
256
256
|
|
|
257
257
|
on the wire, a binary frame is one item of the schema's single binary live field (`pcm16(z, rate, channels)` or `media(z, contentType)`); a json text frame is a partial object keyed by field name: an item of a live field (`{"events": {"type": "interrupt"}}`) or a new value for an ordinary field (`{"effect": "robot"}`). frames that do not fit the input schema are answered with `{"error": {"field", "message"}}` and skipped. `live.send({...})` sends the output binary field as a binary frame and everything else as one json frame; unknown output fields throw.
|
|
258
258
|
|
|
259
|
+
### progress from a stream function
|
|
260
|
+
|
|
261
|
+
a stream function may also be an async generator (`async *dictate(input, socket)`). each yield is a progress snapshot of the task's output: it replaces the task's `output` and reaches clients as a task update (sse `/tasks/{id}/stream`), not over the socket — the socket carries the live fields. yield cumulative snapshots; the last yield is the result, so `output_meta` goes there, and a generator that ends without yielding fails the task (`NoOutput`).
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
export const DictateInput = z.object({
|
|
265
|
+
audio: createStreamSchema(z, pcm16(z, 16000)), // mic frames arrive over the socket
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export const DictateOutput = z.object({
|
|
269
|
+
text: z.string().default(""),
|
|
270
|
+
partial: z.boolean().default(true),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export class App {
|
|
274
|
+
async *dictate(input, socket) {
|
|
275
|
+
const live = new Live(socket, input, DictateInput, DictateOutput);
|
|
276
|
+
let window = Buffer.alloc(0);
|
|
277
|
+
for await (const { field, value } of live) {
|
|
278
|
+
if (field === "audio") {
|
|
279
|
+
window = Buffer.concat([window, value]);
|
|
280
|
+
if (window.length >= 32000) { // one second of 16 khz s16le
|
|
281
|
+
yield { text: await this.transcribe(window) }; // the transcript so far
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
yield {
|
|
286
|
+
text: await this.transcribe(window),
|
|
287
|
+
partial: false,
|
|
288
|
+
output_meta: { inputs: [], outputs: [] },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
the client closing the socket ends the `for await`, so the final yield goes after the loop. both channels can be used at once: `live.send({ text })` pushes a partial over the socket for the lowest latency, while the yields build the transcript the task stores.
|
|
295
|
+
|
|
296
|
+
### frames dropped when the app falls behind
|
|
297
|
+
|
|
298
|
+
binary frames are perishable media, so the kernel keeps at most 256 of them queued for the app (about five seconds of 20 ms audio) and drops the oldest when the app reads slower than the client sends — latency stays bounded instead of growing for ever. text frames (commands and patches) are never dropped. `socket.dropped` counts the binary frames lost so far, and `socket.binaryBacklog = null` turns dropping off when every frame matters (a file sent in chunks).
|
|
299
|
+
|
|
259
300
|
## requirements
|
|
260
301
|
|
|
261
302
|
- node.js 18.0.0 or higher
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { File } from "./file.js";
|
|
|
2
2
|
export type { FileOptions, FileData } from "./file.js";
|
|
3
3
|
export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
|
|
4
4
|
export type { Socket, Frame } from "./socket.js";
|
|
5
|
-
export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, BINARY_SCHEMA_MARKER, } from "./stream.js";
|
|
5
|
+
export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, CLEAR_KEY, BINARY_SCHEMA_MARKER, } from "./stream.js";
|
|
6
6
|
export type { Update } from "./stream.js";
|
|
7
7
|
export { StorageDir, ensureDir } from "./storage.js";
|
|
8
8
|
export type { StorageDirValue } from "./storage.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { File } from "./file.js";
|
|
3
3
|
// Zod schema utilities
|
|
4
4
|
export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
|
|
5
|
-
export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, BINARY_SCHEMA_MARKER, } from "./stream.js";
|
|
5
|
+
export { createStreamSchema, media, pcm16, Live, liveFields, binaryField, STREAM_SCHEMA_MARKER, CLEAR_KEY, BINARY_SCHEMA_MARKER, } from "./stream.js";
|
|
6
6
|
// Storage directories
|
|
7
7
|
export { StorageDir, ensureDir } from "./storage.js";
|
|
8
8
|
// Download utility
|
package/dist/socket.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type Frame = Buffer | string;
|
|
|
31
31
|
export interface Socket {
|
|
32
32
|
readonly id: string;
|
|
33
33
|
metadata: Record<string, unknown>;
|
|
34
|
+
/** How many binary frames the kernel has dropped because the app read too slowly. */
|
|
35
|
+
readonly dropped: number;
|
|
36
|
+
/** The most binary frames kept queued (256); null keeps every one of them. */
|
|
37
|
+
binaryBacklog: number | null;
|
|
34
38
|
/** True once the client is gone or the app closed the socket. */
|
|
35
39
|
readonly closed: boolean;
|
|
36
40
|
/** The next frame from the client, or null once it is gone. */
|
package/dist/stream.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ import type { Socket } from "./socket.js";
|
|
|
51
51
|
* The kernel's zodToJsonSchema reads this and emits `format: "stream"`.
|
|
52
52
|
*/
|
|
53
53
|
export declare const STREAM_SCHEMA_MARKER: unique symbol;
|
|
54
|
+
/**
|
|
55
|
+
* A control frame, `{"$clear": "audio"}`: drop what has been buffered of a
|
|
56
|
+
* live output field. Reserved keys start with `$`, which no field name can.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CLEAR_KEY = "$clear";
|
|
54
59
|
/**
|
|
55
60
|
* Marks a binary item schema. The value on `_def` is the content type.
|
|
56
61
|
* The kernel's zodToJsonSchema reads this and emits
|
|
@@ -113,6 +118,11 @@ export declare function binaryField(objectSchema: any): string | null;
|
|
|
113
118
|
*
|
|
114
119
|
* A frame that does not fit the input schema is answered with
|
|
115
120
|
* `{"error": {"field": ..., "message": ...}}` and skipped; the stream goes on.
|
|
121
|
+
*
|
|
122
|
+
* The function may instead be an async generator (`async *talk(inputData, socket)`):
|
|
123
|
+
* each yield is a cumulative snapshot of the task's output, sent as a task update
|
|
124
|
+
* rather than over the socket, and the last yield (after the loop, once the client
|
|
125
|
+
* closes) is the result.
|
|
116
126
|
*/
|
|
117
127
|
export declare class Live {
|
|
118
128
|
readonly socket: Socket;
|
|
@@ -122,6 +132,8 @@ export declare class Live {
|
|
|
122
132
|
private readonly inBinary;
|
|
123
133
|
private readonly outShape;
|
|
124
134
|
private readonly outBinary;
|
|
135
|
+
private readonly outLive;
|
|
136
|
+
private refusedBinary;
|
|
125
137
|
constructor(socket: Socket, inputData: Record<string, unknown>, inputSchema: any, outputSchema: any);
|
|
126
138
|
[Symbol.asyncIterator](): AsyncGenerator<Update, void, undefined>;
|
|
127
139
|
private frames;
|
|
@@ -135,5 +147,10 @@ export declare class Live {
|
|
|
135
147
|
* `await live.send({ effect: "echo" })` are JSON frames. Keys in one call
|
|
136
148
|
* other than the binary field go out together as one JSON frame.
|
|
137
149
|
*/
|
|
150
|
+
/**
|
|
151
|
+
* Tell the caller to drop what it has buffered of a live output field:
|
|
152
|
+
* the queued audio of an answer the user just talked over.
|
|
153
|
+
*/
|
|
154
|
+
clear(field: string): Promise<void>;
|
|
138
155
|
send(fields: Record<string, unknown>): Promise<void>;
|
|
139
156
|
}
|
package/dist/stream.js
CHANGED
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
* The kernel's zodToJsonSchema reads this and emits `format: "stream"`.
|
|
51
51
|
*/
|
|
52
52
|
export const STREAM_SCHEMA_MARKER = Symbol.for("inferencesh.streamSchema");
|
|
53
|
+
/**
|
|
54
|
+
* A control frame, `{"$clear": "audio"}`: drop what has been buffered of a
|
|
55
|
+
* live output field. Reserved keys start with `$`, which no field name can.
|
|
56
|
+
*/
|
|
57
|
+
export const CLEAR_KEY = "$clear";
|
|
53
58
|
/**
|
|
54
59
|
* Marks a binary item schema. The value on `_def` is the content type.
|
|
55
60
|
* The kernel's zodToJsonSchema reads this and emits
|
|
@@ -199,6 +204,11 @@ function firstIssue(result) {
|
|
|
199
204
|
*
|
|
200
205
|
* A frame that does not fit the input schema is answered with
|
|
201
206
|
* `{"error": {"field": ..., "message": ...}}` and skipped; the stream goes on.
|
|
207
|
+
*
|
|
208
|
+
* The function may instead be an async generator (`async *talk(inputData, socket)`):
|
|
209
|
+
* each yield is a cumulative snapshot of the task's output, sent as a task update
|
|
210
|
+
* rather than over the socket, and the last yield (after the loop, once the client
|
|
211
|
+
* closes) is the result.
|
|
202
212
|
*/
|
|
203
213
|
export class Live {
|
|
204
214
|
socket;
|
|
@@ -208,6 +218,8 @@ export class Live {
|
|
|
208
218
|
inBinary;
|
|
209
219
|
outShape;
|
|
210
220
|
outBinary;
|
|
221
|
+
outLive;
|
|
222
|
+
refusedBinary = false;
|
|
211
223
|
constructor(socket, inputData, inputSchema, outputSchema) {
|
|
212
224
|
this.socket = socket;
|
|
213
225
|
this.input = inputData;
|
|
@@ -216,6 +228,7 @@ export class Live {
|
|
|
216
228
|
this.inBinary = binaryField(inputSchema);
|
|
217
229
|
this.outShape = shapeOf(outputSchema);
|
|
218
230
|
this.outBinary = binaryField(outputSchema);
|
|
231
|
+
this.outLive = liveFields(outputSchema);
|
|
219
232
|
}
|
|
220
233
|
async *[Symbol.asyncIterator]() {
|
|
221
234
|
for await (const frame of this.frames()) {
|
|
@@ -240,7 +253,12 @@ export class Live {
|
|
|
240
253
|
async read(frame) {
|
|
241
254
|
if (isBinaryFrame(frame)) {
|
|
242
255
|
if (this.inBinary === null) {
|
|
243
|
-
|
|
256
|
+
// Once: a caller streaming audio to the wrong function would
|
|
257
|
+
// otherwise get an error frame for every frame it sends.
|
|
258
|
+
if (!this.refusedBinary) {
|
|
259
|
+
this.refusedBinary = true;
|
|
260
|
+
await this.refuse(null, "this function takes no binary frames");
|
|
261
|
+
}
|
|
244
262
|
return [];
|
|
245
263
|
}
|
|
246
264
|
return [{ field: this.inBinary, value: toBuffer(frame) }];
|
|
@@ -258,30 +276,25 @@ export class Live {
|
|
|
258
276
|
}
|
|
259
277
|
const updates = [];
|
|
260
278
|
for (const [key, raw] of Object.entries(patch)) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
const result = this.inLive[key].safeParse(raw);
|
|
267
|
-
if (!result.success) {
|
|
268
|
-
await this.refuse(key, firstIssue(result));
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
updates.push({ field: key, value: result.data });
|
|
279
|
+
const live = Object.prototype.hasOwnProperty.call(this.inLive, key);
|
|
280
|
+
if (!live && !Object.prototype.hasOwnProperty.call(this.inShape, key)) {
|
|
281
|
+
await this.refuse(key, "no such field");
|
|
282
|
+
continue;
|
|
272
283
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
await this.refuse(key, firstIssue(result));
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
this.input[key] = result.data;
|
|
280
|
-
updates.push({ field: key, value: result.data });
|
|
284
|
+
if (key === this.inBinary) {
|
|
285
|
+
await this.refuse(key, "send this field's items as binary frames");
|
|
286
|
+
continue;
|
|
281
287
|
}
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
// A live field's items are validated against the item schema, an
|
|
289
|
+
// ordinary field against its own and then applied to the input.
|
|
290
|
+
const result = (live ? this.inLive[key] : this.inShape[key]).safeParse(raw);
|
|
291
|
+
if (!result.success) {
|
|
292
|
+
await this.refuse(key, firstIssue(result));
|
|
293
|
+
continue;
|
|
284
294
|
}
|
|
295
|
+
if (!live)
|
|
296
|
+
this.input[key] = result.data;
|
|
297
|
+
updates.push({ field: key, value: result.data });
|
|
285
298
|
}
|
|
286
299
|
return updates;
|
|
287
300
|
}
|
|
@@ -296,6 +309,16 @@ export class Live {
|
|
|
296
309
|
* `await live.send({ effect: "echo" })` are JSON frames. Keys in one call
|
|
297
310
|
* other than the binary field go out together as one JSON frame.
|
|
298
311
|
*/
|
|
312
|
+
/**
|
|
313
|
+
* Tell the caller to drop what it has buffered of a live output field:
|
|
314
|
+
* the queued audio of an answer the user just talked over.
|
|
315
|
+
*/
|
|
316
|
+
async clear(field) {
|
|
317
|
+
if (!Object.prototype.hasOwnProperty.call(this.outLive, field)) {
|
|
318
|
+
throw new Error(`the output schema has no live field "${field}"`);
|
|
319
|
+
}
|
|
320
|
+
await this.socket.send({ [CLEAR_KEY]: field });
|
|
321
|
+
}
|
|
299
322
|
async send(fields) {
|
|
300
323
|
const patch = {};
|
|
301
324
|
for (const [key, value] of Object.entries(fields)) {
|
package/package.json
CHANGED