@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 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
- await this.refuse(null, "this function takes no binary frames");
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
- if (Object.prototype.hasOwnProperty.call(this.inLive, key)) {
262
- if (key === this.inBinary) {
263
- await this.refuse(key, "send this field's items as binary frames");
264
- continue;
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
- else if (Object.prototype.hasOwnProperty.call(this.inShape, key)) {
274
- const result = this.inShape[key].safeParse(raw);
275
- if (!result.success) {
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
- else {
283
- await this.refuse(key, "no such field");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/app",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "App framework for building inference.sh apps — File handling, output metadata, storage utilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",