@effect-uai/mistral 0.17.0 → 0.19.0-dev-20261109183959

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.
Files changed (57) hide show
  1. package/README.md +4 -4
  2. package/dist/Mistral.d.mts +1 -1
  3. package/dist/Mistral.d.mts.map +1 -1
  4. package/dist/Mistral.mjs +4 -3
  5. package/dist/Mistral.mjs.map +1 -1
  6. package/dist/MistralRealtimeTranscriber.d.mts +2 -2
  7. package/dist/MistralRealtimeTranscriber.mjs +2 -2
  8. package/dist/MistralRealtimeTranscriber.mjs.map +1 -1
  9. package/dist/MistralSynthesizer.d.mts +1 -1
  10. package/dist/MistralSynthesizer.d.mts.map +1 -1
  11. package/dist/MistralSynthesizer.mjs +5 -4
  12. package/dist/MistralSynthesizer.mjs.map +1 -1
  13. package/dist/MistralTranscriber.d.mts +1 -1
  14. package/dist/MistralTranscriber.mjs +2 -2
  15. package/dist/MistralTranscriber.mjs.map +1 -1
  16. package/dist/audioCodec.d.mts.map +1 -1
  17. package/dist/audioCodec.mjs +3 -2
  18. package/dist/audioCodec.mjs.map +1 -1
  19. package/dist/codec.d.mts +3 -3
  20. package/dist/codec.d.mts.map +1 -1
  21. package/dist/codec.mjs +8 -6
  22. package/dist/codec.mjs.map +1 -1
  23. package/dist/codec.test.mjs +9 -3
  24. package/dist/codec.test.mjs.map +1 -1
  25. package/dist/dist-DHer1cql.mjs +1533 -0
  26. package/dist/dist-DHer1cql.mjs.map +1 -0
  27. package/dist/http.d.mts +1 -1
  28. package/dist/http.mjs.map +1 -1
  29. package/dist/{test.DNmyFkvJ-N3QqMRwZ.mjs → index.C4tKZ0yW-CUBm3Bmz.mjs} +9446 -5818
  30. package/dist/index.C4tKZ0yW-CUBm3Bmz.mjs.map +1 -0
  31. package/dist/onHalt.test.mjs +2 -2
  32. package/dist/onHalt.test.mjs.map +1 -1
  33. package/dist/{realtimeStt-BcthMc57.d.mts → realtimeStt-CcuQ6EZS.d.mts} +7 -1
  34. package/dist/realtimeStt-CcuQ6EZS.d.mts.map +1 -0
  35. package/dist/realtimeStt.d.mts +1 -1
  36. package/dist/realtimeStt.mjs +27 -30
  37. package/dist/realtimeStt.mjs.map +1 -1
  38. package/dist/realtimeStt.test.d.mts +1 -0
  39. package/dist/realtimeStt.test.mjs +317 -0
  40. package/dist/realtimeStt.test.mjs.map +1 -0
  41. package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-EAsCxrDo.mjs} +2 -1
  42. package/package.json +6 -6
  43. package/src/Mistral.ts +3 -1
  44. package/src/MistralRealtimeTranscriber.ts +1 -1
  45. package/src/MistralSynthesizer.ts +4 -13
  46. package/src/MistralTranscriber.ts +1 -1
  47. package/src/audioCodec.ts +3 -2
  48. package/src/codec.test.ts +9 -2
  49. package/src/codec.ts +6 -3
  50. package/src/http.ts +1 -1
  51. package/src/onHalt.test.ts +1 -1
  52. package/src/realtimeStt.test.ts +116 -0
  53. package/src/realtimeStt.ts +68 -64
  54. package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
  55. package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
  56. package/dist/realtimeStt-BcthMc57.d.mts.map +0 -1
  57. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { make } from "./Mistral.mjs";
2
- import { n as describe, r as it, t as globalExpect } from "./test.DNmyFkvJ-N3QqMRwZ.mjs";
2
+ import { n as globalExpect, o as describe, s as it } from "./index.C4tKZ0yW-CUBm3Bmz.mjs";
3
3
  import { Effect, Exit, Redacted, Stream } from "effect";
4
- import { HttpClient, HttpClientResponse } from "effect/unstable/http";
4
+ import { HttpClient, HttpClientResponse } from "effect/http";
5
5
  //#region src/onHalt.test.ts
6
6
  const clientWithBody = (makeBody) => HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response(makeBody(), {
7
7
  status: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"onHalt.test.mjs","names":["makeMistral"],"sources":["../src/onHalt.test.ts"],"sourcesContent":["import { Effect, Exit, Redacted, Stream } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"effect/unstable/http\"\nimport { describe, expect, it } from \"vitest\"\nimport { TurnEvent } from \"@effect-uai/core/Turn\"\nimport { make as makeMistral } from \"./Mistral.js\"\n\n// An HttpClient backed by a scripted SSE body\nconst clientWithBody = (makeBody: () => ReadableStream<Uint8Array>): HttpClient.HttpClient =>\n HttpClient.make((request) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(makeBody(), {\n status: 200,\n headers: { \"content-type\": \"text/event-stream\" },\n }),\n ),\n ),\n )\n\nconst enc = new TextEncoder()\nconst sse = (obj: unknown) => enc.encode(`data: ${JSON.stringify(obj)}\\n\\n`)\n\nconst runStream = (client: HttpClient.HttpClient) =>\n Effect.gen(function* () {\n const svc = yield* makeMistral({ apiKey: Redacted.make(\"k\") })\n const events: Array<TurnEvent> = []\n const exit = yield* svc.streamTurn({ model: \"mistral-small-latest\", history: [] }).pipe(\n Stream.runForEach((e) => Effect.sync(() => events.push(e))),\n Effect.exit,\n )\n return { tags: events.map((e) => e._tag), events, exit }\n }).pipe(Effect.provideService(HttpClient.HttpClient, client))\n\ndescribe(\"Mistral streamTurn: truncated turn presented as complete\", () => {\n it(\"truncated EOF (no finish_reason) must not synthesize a complete turn\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.close() // no finish_reason, no [DONE]\n },\n })\n const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(tags).toContain(\"TextDelta\")\n expect(tags).not.toContain(\"TurnComplete\")\n })\n\n it(\"mid-stream error must surface as a failure, not a fake TurnComplete\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.error(new Error(\"connection reset\"))\n },\n })\n const { tags, exit } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(Exit.isFailure(exit)).toBe(true)\n expect(tags).not.toContain(\"TurnComplete\")\n })\n\n it(\"a well-formed stream (finish_reason present) still completes\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.enqueue(sse({ choices: [{ delta: {}, finish_reason: \"stop\" }] }))\n controller.enqueue(enc.encode(\"data: [DONE]\\n\\n\"))\n controller.close()\n },\n })\n const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(tags).toContain(\"TurnComplete\")\n })\n})\n"],"mappings":";;;;;AAOA,MAAM,kBAAkB,aACtB,WAAW,MAAM,YACf,OAAO,QACL,mBAAmB,QACjB,SACA,IAAI,SAAS,SAAS,GAAG;CACvB,QAAQ;CACR,SAAS,EAAE,gBAAgB,oBAAoB;AACjD,CAAC,CACH,CACF,CACF;AAEF,MAAM,MAAM,IAAI,YAAY;AAC5B,MAAM,OAAO,QAAiB,IAAI,OAAO,SAAS,KAAK,UAAU,GAAG,EAAE,KAAK;AAE3E,MAAM,aAAa,WACjB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAOA,KAAY,EAAE,QAAQ,SAAS,KAAK,GAAG,EAAE,CAAC;CAC7D,MAAM,SAA2B,CAAC;CAClC,MAAM,OAAO,OAAO,IAAI,WAAW;EAAE,OAAO;EAAwB,SAAS,CAAC;CAAE,CAAC,CAAC,CAAC,KACjF,OAAO,YAAY,MAAM,OAAO,WAAW,OAAO,KAAK,CAAC,CAAC,CAAC,GAC1D,OAAO,IACT;CACA,OAAO;EAAE,MAAM,OAAO,KAAK,MAAM,EAAE,IAAI;EAAG;EAAQ;CAAK;AACzD,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,WAAW,YAAY,MAAM,CAAC;AAE9D,SAAS,kEAAkE;CACzE,GAAG,wEAAwE,YAAY;EACrF,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,MAAM;EACnB,EACF,CAAC;EACH,MAAM,EAAE,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EACxE,aAAO,IAAI,CAAC,CAAC,UAAU,WAAW;EAClC,aAAO,IAAI,CAAC,CAAC,IAAI,UAAU,cAAc;CAC3C,CAAC;CAED,GAAG,uEAAuE,YAAY;EACpF,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,sBAAM,IAAI,MAAM,kBAAkB,CAAC;EAChD,EACF,CAAC;EACH,MAAM,EAAE,MAAM,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EAC9E,aAAO,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;EACtC,aAAO,IAAI,CAAC,CAAC,IAAI,UAAU,cAAc;CAC3C,CAAC;CAED,GAAG,gEAAgE,YAAY;EAC7E,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC;IAAE,OAAO,CAAC;IAAG,eAAe;GAAO,CAAC,EAAE,CAAC,CAAC;GAC3E,WAAW,QAAQ,IAAI,OAAO,kBAAkB,CAAC;GACjD,WAAW,MAAM;EACnB,EACF,CAAC;EACH,MAAM,EAAE,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EACxE,aAAO,IAAI,CAAC,CAAC,UAAU,cAAc;CACvC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"onHalt.test.mjs","names":["makeMistral"],"sources":["../src/onHalt.test.ts"],"sourcesContent":["import { Effect, Exit, Redacted, Stream } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"effect/http\"\nimport { describe, expect, it } from \"vitest\"\nimport { TurnEvent } from \"@effect-uai/core/Turn\"\nimport { make as makeMistral } from \"./Mistral.js\"\n\n// An HttpClient backed by a scripted SSE body\nconst clientWithBody = (makeBody: () => ReadableStream<Uint8Array>): HttpClient.HttpClient =>\n HttpClient.make((request) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(makeBody(), {\n status: 200,\n headers: { \"content-type\": \"text/event-stream\" },\n }),\n ),\n ),\n )\n\nconst enc = new TextEncoder()\nconst sse = (obj: unknown) => enc.encode(`data: ${JSON.stringify(obj)}\\n\\n`)\n\nconst runStream = (client: HttpClient.HttpClient) =>\n Effect.gen(function* () {\n const svc = yield* makeMistral({ apiKey: Redacted.make(\"k\") })\n const events: Array<TurnEvent> = []\n const exit = yield* svc.streamTurn({ model: \"mistral-small-latest\", history: [] }).pipe(\n Stream.runForEach((e) => Effect.sync(() => events.push(e))),\n Effect.exit,\n )\n return { tags: events.map((e) => e._tag), events, exit }\n }).pipe(Effect.provideService(HttpClient.HttpClient, client))\n\ndescribe(\"Mistral streamTurn: truncated turn presented as complete\", () => {\n it(\"truncated EOF (no finish_reason) must not synthesize a complete turn\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.close() // no finish_reason, no [DONE]\n },\n })\n const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(tags).toContain(\"TextDelta\")\n expect(tags).not.toContain(\"TurnComplete\")\n })\n\n it(\"mid-stream error must surface as a failure, not a fake TurnComplete\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.error(new Error(\"connection reset\"))\n },\n })\n const { tags, exit } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(Exit.isFailure(exit)).toBe(true)\n expect(tags).not.toContain(\"TurnComplete\")\n })\n\n it(\"a well-formed stream (finish_reason present) still completes\", async () => {\n const body = () =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(sse({ choices: [{ delta: { content: \"Hel\" } }] }))\n controller.enqueue(sse({ choices: [{ delta: {}, finish_reason: \"stop\" }] }))\n controller.enqueue(enc.encode(\"data: [DONE]\\n\\n\"))\n controller.close()\n },\n })\n const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))\n expect(tags).toContain(\"TurnComplete\")\n })\n})\n"],"mappings":";;;;;AAOA,MAAM,kBAAkB,aACtB,WAAW,MAAM,YACf,OAAO,QACL,mBAAmB,QACjB,SACA,IAAI,SAAS,SAAS,GAAG;CACvB,QAAQ;CACR,SAAS,EAAE,gBAAgB,oBAAoB;AACjD,CAAC,CACH,CACF,CACF;AAEF,MAAM,MAAM,IAAI,YAAY;AAC5B,MAAM,OAAO,QAAiB,IAAI,OAAO,SAAS,KAAK,UAAU,GAAG,EAAE,KAAK;AAE3E,MAAM,aAAa,WACjB,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAOA,KAAY,EAAE,QAAQ,SAAS,KAAK,GAAG,EAAE,CAAC;CAC7D,MAAM,SAA2B,CAAC;CAClC,MAAM,OAAO,OAAO,IAAI,WAAW;EAAE,OAAO;EAAwB,SAAS,CAAC;CAAE,CAAC,CAAC,CAAC,KACjF,OAAO,YAAY,MAAM,OAAO,WAAW,OAAO,KAAK,CAAC,CAAC,CAAC,GAC1D,OAAO,IACT;CACA,OAAO;EAAE,MAAM,OAAO,KAAK,MAAM,EAAE,IAAI;EAAG;EAAQ;CAAK;AACzD,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,WAAW,YAAY,MAAM,CAAC;AAE9D,SAAS,kEAAkE;CACzE,GAAG,wEAAwE,YAAY;EACrF,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,MAAM;EACnB,EACF,CAAC;EACH,MAAM,EAAE,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EACxE,aAAO,IAAI,CAAC,CAAC,UAAU,WAAW;EAClC,aAAO,IAAI,CAAC,CAAC,IAAI,UAAU,cAAc;CAC3C,CAAC;CAED,GAAG,uEAAuE,YAAY;EACpF,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,sBAAM,IAAI,MAAM,kBAAkB,CAAC;EAChD,EACF,CAAC;EACH,MAAM,EAAE,MAAM,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EAC9E,aAAO,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;EACtC,aAAO,IAAI,CAAC,CAAC,IAAI,UAAU,cAAc;CAC3C,CAAC;CAED,GAAG,gEAAgE,YAAY;EAC7E,MAAM,aACJ,IAAI,eAA2B,EAC7B,MAAM,YAAY;GAChB,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;GACpE,WAAW,QAAQ,IAAI,EAAE,SAAS,CAAC;IAAE,OAAO,CAAC;IAAG,eAAe;GAAO,CAAC,EAAE,CAAC,CAAC;GAC3E,WAAW,QAAQ,IAAI,OAAO,kBAAkB,CAAC;GACjD,WAAW,MAAM;EACnB,EACF,CAAC;EACH,MAAM,EAAE,SAAS,MAAM,OAAO,WAAW,UAAU,eAAe,IAAI,CAAC,CAAC;EACxE,aAAO,IAAI,CAAC,CAAC,UAAU,cAAc;CACvC,CAAC;AACH,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { Duration, Redacted, Stream } from "effect";
2
2
  import * as AiError from "@effect-uai/core/AiError";
3
3
  import { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber";
4
+ import * as Socket from "effect/socket/Socket";
4
5
  import { TranscriptEvent } from "@effect-uai/core/Transcript";
5
6
  //#region src/realtimeStt.d.ts
6
7
  type Config = {
@@ -14,8 +15,13 @@ type Config = {
14
15
  * adapter delimits turns. Default 700 ms.
15
16
  */
16
17
  readonly utteranceSilence?: Duration.Duration;
18
+ /**
19
+ * Build the socket yourself, for a proxy or an in-memory transport. The
20
+ * default attaches the bearer token through the `ws` package.
21
+ */
22
+ readonly webSocket?: Socket.WebSocketConstructor["Service"];
17
23
  };
18
24
  declare const streamTranscription: (cfg: Config) => <E, R>(audioIn: Stream.Stream<Uint8Array, E, R>, request: CommonStreamTranscribeRequest) => Stream.Stream<TranscriptEvent, AiError.AiError | E, R>;
19
25
  //#endregion
20
26
  export { streamTranscription as n, Config as t };
21
- //# sourceMappingURL=realtimeStt-BcthMc57.d.mts.map
27
+ //# sourceMappingURL=realtimeStt-CcuQ6EZS.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeStt-CcuQ6EZS.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;KA+CY;WACD,QAAQ,SAAS;WACjB;;WAEA,uBAAuB,SAAS;;;;;;WAMhC,mBAAmB,SAAS;;;;;WAK5B,YAAY,OAAO;;cAsJjB,sBAAmB,KACxB,YACL,GAAG,GAAC,SACM,OAAO,OAAO,YAAY,GAAG,IAAE,SAC/B,kCACR,OAAO,OAAO,iBAAiB,QAAQ,UAAU,GAAG"}
@@ -1,2 +1,2 @@
1
- import { n as streamTranscription, t as Config } from "./realtimeStt-BcthMc57.mjs";
1
+ import { n as streamTranscription, t as Config } from "./realtimeStt-CcuQ6EZS.mjs";
2
2
  export { Config, streamTranscription };
@@ -1,7 +1,9 @@
1
- import { Clock, Duration, Effect, Encoding, Match, Queue, Redacted, Ref, Schema, Stream } from "effect";
1
+ import { Clock, Duration, Effect, Match, Option, Predicate, Redacted, Ref, Schema, Stream } from "effect";
2
2
  import * as AiError from "@effect-uai/core/AiError";
3
- import * as Socket from "effect/unstable/socket/Socket";
3
+ import { Base64 } from "effect/encoding";
4
+ import * as Socket from "effect/socket/Socket";
4
5
  import * as JSONL from "@effect-uai/core/JSONL";
6
+ import * as WebSocketSession from "@effect-uai/core/WebSocketSession";
5
7
  import { WebSocket } from "ws";
6
8
  //#region src/realtimeStt.ts
7
9
  /**
@@ -50,7 +52,7 @@ const sessionUpdateFrame = (cfg) => JSON.stringify({
50
52
  });
51
53
  const audioAppendFrame = (bytes) => JSON.stringify({
52
54
  type: "input_audio.append",
53
- audio: Encoding.encodeBase64(bytes)
55
+ audio: Base64.encode(bytes)
54
56
  });
55
57
  const audioEndFrame = JSON.stringify({ type: "input_audio.end" });
56
58
  const ServerEvent = Schema.Union([
@@ -75,60 +77,55 @@ const ServerEvent = Schema.Union([
75
77
  })
76
78
  ]);
77
79
  const decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent);
78
- const emitFinal = (queue, state, override) => Effect.gen(function* () {
80
+ const takeFinal = (state, override) => Effect.gen(function* () {
79
81
  const acc = yield* Ref.getAndSet(state.text, "");
80
82
  const text = (override !== void 0 && override.length > 0 ? override : acc).trim();
81
- if (text.length > 0) yield* Queue.offer(queue, {
83
+ return text.length > 0 ? {
82
84
  _tag: "final",
83
85
  text
84
- });
86
+ } : void 0;
85
87
  });
86
- const handleServerMessage = (queue, state) => (raw) => Effect.gen(function* () {
88
+ const frameToEvent = (state) => (raw) => Effect.gen(function* () {
87
89
  const json = yield* JSONL.parseSafe(raw);
88
- if (json === void 0) return;
90
+ if (json === void 0) return void 0;
89
91
  const decoded = yield* decodeServerEvent(json).pipe(Effect.option);
90
- if (decoded._tag === "None") return;
91
- yield* Match.value(decoded.value).pipe(Match.when({ type: "transcription.text.delta" }, (m) => Effect.gen(function* () {
92
+ if (Option.isNone(decoded)) return void 0;
93
+ return yield* Match.value(decoded.value).pipe(Match.when({ type: "transcription.text.delta" }, (m) => Effect.gen(function* () {
92
94
  const now = yield* Clock.currentTimeMillis;
93
95
  const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text);
94
96
  yield* Ref.set(state.lastActivityMs, now);
95
- yield* Queue.offer(queue, {
97
+ return {
96
98
  _tag: "partial",
97
99
  text: next
98
- });
99
- })), Match.when({ type: "transcription.done" }, (m) => emitFinal(queue, state, m.text)), Match.when({ type: "error" }, (m) => Queue.offer(queue, {
100
+ };
101
+ })), Match.when({ type: "transcription.done" }, (m) => takeFinal(state, m.text)), Match.when({ type: "error" }, (m) => Effect.succeed({
100
102
  _tag: "error",
101
103
  ...m.error.code != null && { code: String(m.error.code) },
102
104
  message: m.error.message
103
- })), Match.orElse(() => Effect.void));
105
+ })), Match.orElse(() => Effect.succeed(void 0)));
104
106
  });
105
- const silenceFinalizer = (queue, state, silence) => {
107
+ const silenceFinals = (state, silence) => {
106
108
  const silenceMs = Duration.toMillis(silence);
107
- return Effect.gen(function* () {
109
+ return Stream.tick("150 millis").pipe(Stream.mapEffect(() => Effect.gen(function* () {
108
110
  const now = yield* Clock.currentTimeMillis;
109
111
  const last = yield* Ref.get(state.lastActivityMs);
110
- if ((yield* Ref.get(state.text)).trim().length > 0 && now - last >= silenceMs) yield* emitFinal(queue, state);
111
- yield* Effect.sleep("150 millis");
112
- }).pipe(Effect.forever);
112
+ return (yield* Ref.get(state.text)).trim().length > 0 && now - last >= silenceMs ? yield* takeFinal(state) : void 0;
113
+ })), Stream.filter(Predicate.isNotUndefined));
113
114
  };
114
115
  const authedWsConstructor = (cfg) => (url) => new WebSocket(url, void 0, { headers: { Authorization: `Bearer ${Redacted.value(cfg.apiKey)}` } });
115
116
  const streamTranscription = (cfg) => (audioIn, request) => Stream.unwrap(Effect.gen(function* () {
116
117
  yield* ensureInputFormat(request.inputFormat);
117
- const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg)));
118
- const queue = yield* Queue.bounded(64);
118
+ const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model)).pipe(Effect.provideService(Socket.WebSocketConstructor, cfg.webSocket ?? authedWsConstructor(cfg)));
119
119
  const state = {
120
120
  text: yield* Ref.make(""),
121
121
  lastActivityMs: yield* Ref.make(0)
122
122
  };
123
- const write = yield* socket.writer;
124
- yield* Effect.gen(function* () {
125
- yield* write(sessionUpdateFrame(cfg));
126
- yield* Stream.runForEach(audioIn, (bytes) => write(audioAppendFrame(bytes)));
127
- yield* write(audioEndFrame);
128
- }).pipe(Effect.tapError((cause) => Effect.logWarning("[voxtral-realtime] audio send failed", { cause })), Effect.ignore, Effect.forkScoped);
129
- yield* silenceFinalizer(queue, state, cfg.utteranceSilence ?? DEFAULT_SILENCE).pipe(Effect.forkScoped);
130
- yield* socket.runString(handleServerMessage(queue, state)).pipe(Effect.tapError((cause) => Effect.logWarning("[voxtral-realtime] socket closed", { cause })), Effect.ensuring(Queue.end(queue)), Effect.forkScoped);
131
- return Stream.fromQueue(queue);
123
+ const { write } = yield* socket.writer;
124
+ const asAiError = WebSocketSession.toAiError("mistral");
125
+ const frames = Stream.fromPull(Socket.readerString(socket)).pipe(Stream.scoped, Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty), Stream.mapError(asAiError), Stream.mapEffect(frameToEvent(state)), Stream.filter(Predicate.isNotUndefined));
126
+ const events = Stream.merge(frames, silenceFinals(state, cfg.utteranceSilence ?? DEFAULT_SILENCE), { haltStrategy: "left" });
127
+ const outgoing = Stream.concat(Stream.concat(Stream.make(sessionUpdateFrame(cfg)), Stream.map(audioIn, audioAppendFrame)), Stream.make(audioEndFrame)).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)));
128
+ return Stream.merge(events, Stream.drain(outgoing), { haltStrategy: "left" });
132
129
  }));
133
130
  //#endregion
134
131
  export { streamTranscription };
@@ -1 +1 @@
1
- {"version":3,"file":"realtimeStt.mjs","names":["WSWebSocket"],"sources":["../src/realtimeStt.ts"],"sourcesContent":["/**\n * Voxtral Realtime STT over WebSocket.\n *\n * Protocol mirrors the `mistralai[realtime]` Python client:\n * - URL: `wss://api.mistral.ai/v1/audio/transcriptions/realtime?model=…`\n * (the model is a query param, not a session field).\n * - Auth: `Authorization: Bearer …` header on the upgrade. The browser\n * `WebSocket` API can't set headers, so this module uses the `ws` peer\n * dep; `ws` is only pulled in transitively via\n * `MistralRealtimeTranscriber` (the sync `MistralTranscriber` stays free\n * of it).\n * - Client → server (JSON): `session.update` (audio_format +\n * target_streaming_delay_ms), then `input_audio.append` with base64 PCM,\n * and `input_audio.end` when the mic stream stops.\n * - Server → client (JSON): `session.created` / `session.updated`,\n * `transcription.text.delta` (`{ text }`), `transcription.done`\n * (`{ text }`), `transcription.segment`, `transcription.language`,\n * and `error` (`{ error: { message, code } }`).\n *\n * Voxtral Realtime is a *continuous* transcriber: it streams text deltas and\n * only emits one `transcription.done` at end-of-audio (no server-side\n * utterance segmentation). To satisfy the `Transcriber` contract's `final`\n * semantics for conversational use, this adapter synthesizes a `final` event\n * when the deltas go quiet for `utteranceSilence`.\n */\nimport {\n Cause,\n Clock,\n Duration,\n Effect,\n Encoding,\n Match,\n Queue,\n Redacted,\n Ref,\n Schema,\n Stream,\n} from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { WebSocket as WSWebSocket } from \"ws\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Target latency knob (`target_streaming_delay_ms`); lower = faster, less accurate. */\n readonly targetStreamingDelay?: Duration.Duration\n /**\n * Silence gap after which the accumulated deltas are committed as a synthetic\n * `final`. Voxtral Realtime does no utterance segmentation, so this is how the\n * adapter delimits turns. Default 700 ms.\n */\n readonly utteranceSilence?: Duration.Duration\n}\n\nconst DEFAULT_SILENCE = Duration.millis(700)\n\n// ---------------------------------------------------------------------------\n// AudioFormat gate — Voxtral Realtime ingests pcm_s16le @ 16000 mono only.\n// ---------------------------------------------------------------------------\n\nconst ensureInputFormat = (format: AudioFormat): Effect.Effect<void, AiError.AiError> =>\n format.container === \"raw\" && format.encoding === \"pcm_s16le\" && format.sampleRate === 16000\n ? Effect.void\n : Effect.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"inputFormat\",\n reason: `Voxtral Realtime accepts pcm_s16le @ 16000 mono only. Got ${JSON.stringify(format)}.`,\n }),\n )\n\n// ---------------------------------------------------------------------------\n// URL + client frame builders\n// ---------------------------------------------------------------------------\n\nconst buildWsUrl = (cfg: Config, model: string) => {\n const base = (cfg.baseUrl ?? \"https://api.mistral.ai\").replace(/^http/, \"ws\")\n return `${base}/v1/audio/transcriptions/realtime?model=${encodeURIComponent(model)}`\n}\n\nconst sessionUpdateFrame = (cfg: Config) =>\n JSON.stringify({\n type: \"session.update\",\n session: {\n audio_format: { encoding: \"pcm_s16le\", sample_rate: 16000 },\n ...(cfg.targetStreamingDelay !== undefined && {\n target_streaming_delay_ms: Duration.toMillis(cfg.targetStreamingDelay),\n }),\n },\n })\n\nconst audioAppendFrame = (bytes: Uint8Array) =>\n JSON.stringify({ type: \"input_audio.append\", audio: Encoding.encodeBase64(bytes) })\n\nconst audioEndFrame = JSON.stringify({ type: \"input_audio.end\" })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst ServerEvent = Schema.Union([\n Schema.Struct({ type: Schema.Literal(\"session.created\") }),\n Schema.Struct({ type: Schema.Literal(\"session.updated\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.language\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.segment\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.text.delta\"), text: Schema.String }),\n Schema.Struct({ type: Schema.Literal(\"transcription.done\"), text: Schema.String }),\n Schema.Struct({\n type: Schema.Literal(\"error\"),\n error: Schema.Struct({\n message: Schema.String,\n code: Schema.optional(Schema.NullOr(Schema.Number)),\n }),\n }),\n])\nconst decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent)\n\n/** Mutable turn state shared by the message handler and the silence finalizer. */\ntype TurnState = {\n readonly text: Ref.Ref<string>\n readonly lastActivityMs: Ref.Ref<number>\n}\n\nconst emitFinal = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n override?: string,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n const acc = yield* Ref.getAndSet(state.text, \"\")\n const text = (override !== undefined && override.length > 0 ? override : acc).trim()\n if (text.length > 0) yield* Queue.offer(queue, { _tag: \"final\", text })\n })\n\nconst handleServerMessage =\n (queue: Queue.Queue<TranscriptEvent, Cause.Done>, state: TurnState) => (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerEvent(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n yield* Match.value(decoded.value).pipe(\n Match.when({ type: \"transcription.text.delta\" }, (m) =>\n Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text)\n yield* Ref.set(state.lastActivityMs, now)\n // Cumulative partial so the UI shows the growing sentence.\n yield* Queue.offer(queue, { _tag: \"partial\", text: next })\n }),\n ),\n // End-of-audio: commit whatever's left as the final utterance.\n Match.when({ type: \"transcription.done\" }, (m) => emitFinal(queue, state, m.text)),\n Match.when({ type: \"error\" }, (m) =>\n Queue.offer(queue, {\n _tag: \"error\",\n ...(m.error.code != null && { code: String(m.error.code) }),\n message: m.error.message,\n }),\n ),\n // session.created / .updated / language / segment: no user-visible event.\n Match.orElse(() => Effect.void),\n )\n })\n\n// Background loop: commit a synthetic final once the deltas go quiet.\nconst silenceFinalizer = (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n state: TurnState,\n silence: Duration.Duration,\n): Effect.Effect<never> => {\n const silenceMs = Duration.toMillis(silence)\n return Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const last = yield* Ref.get(state.lastActivityMs)\n const acc = yield* Ref.get(state.text)\n if (acc.trim().length > 0 && now - last >= silenceMs) yield* emitFinal(queue, state)\n yield* Effect.sleep(\"150 millis\")\n }).pipe(Effect.forever)\n}\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n// ---------------------------------------------------------------------------\n\n// `@types/ws`'s WebSocket extends Node's EventEmitter while\n// `globalThis.WebSocket` extends EventTarget; the browser-style surface\n// Effect's Socket reads (`addEventListener` / `send` / `close`) is identical at\n// runtime, hence the single contained cast.\nconst authedWsConstructor =\n (cfg: Config): Socket.WebSocketConstructor[\"Service\"] =>\n (url) =>\n new WSWebSocket(url, undefined, {\n headers: { Authorization: `Bearer ${Redacted.value(cfg.apiKey)}` },\n }) as unknown as globalThis.WebSocket\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* ensureInputFormat(request.inputFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model), {\n // Effect's Socket treats all close codes as errors by default —\n // whitelist standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg)))\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const state: TurnState = {\n text: yield* Ref.make(\"\"),\n lastActivityMs: yield* Ref.make(0),\n }\n const write = yield* socket.writer\n\n // session.update first, then stream base64 PCM frames, then signal end.\n // Fork-scoped so the Stream's downstream scope tears them down on\n // disconnect / cancel.\n yield* Effect.gen(function* () {\n yield* write(sessionUpdateFrame(cfg))\n yield* Stream.runForEach(audioIn, (bytes) => write(audioAppendFrame(bytes)))\n yield* write(audioEndFrame)\n }).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] audio send failed\", { cause }),\n ),\n Effect.ignore,\n Effect.forkScoped,\n )\n\n yield* silenceFinalizer(queue, state, cfg.utteranceSilence ?? DEFAULT_SILENCE).pipe(\n Effect.forkScoped,\n )\n\n // `Queue.end` flushes pending events then ends the stream cleanly;\n // `Queue.shutdown` would drop queued items and interrupt takes. A\n // connection/read failure is logged (otherwise the stream would end\n // silently with no transcripts).\n yield* socket.runString(handleServerMessage(queue, state)).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"[voxtral-realtime] socket closed\", { cause }),\n ),\n Effect.ensuring(Queue.end(queue)),\n Effect.forkScoped,\n )\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"realtimeStt.mjs","names":["WSWebSocket"],"sources":["../src/realtimeStt.ts"],"sourcesContent":["/**\n * Voxtral Realtime STT over WebSocket.\n *\n * Protocol mirrors the `mistralai[realtime]` Python client:\n * - URL: `wss://api.mistral.ai/v1/audio/transcriptions/realtime?model=…`\n * (the model is a query param, not a session field).\n * - Auth: `Authorization: Bearer …` header on the upgrade. The browser\n * `WebSocket` API can't set headers, so this module uses the `ws` peer\n * dep; `ws` is only pulled in transitively via\n * `MistralRealtimeTranscriber` (the sync `MistralTranscriber` stays free\n * of it).\n * - Client → server (JSON): `session.update` (audio_format +\n * target_streaming_delay_ms), then `input_audio.append` with base64 PCM,\n * and `input_audio.end` when the mic stream stops.\n * - Server → client (JSON): `session.created` / `session.updated`,\n * `transcription.text.delta` (`{ text }`), `transcription.done`\n * (`{ text }`), `transcription.segment`, `transcription.language`,\n * and `error` (`{ error: { message, code } }`).\n *\n * Voxtral Realtime is a *continuous* transcriber: it streams text deltas and\n * only emits one `transcription.done` at end-of-audio (no server-side\n * utterance segmentation). To satisfy the `Transcriber` contract's `final`\n * semantics for conversational use, this adapter synthesizes a `final` event\n * when the deltas go quiet for `utteranceSilence`.\n */\nimport {\n Clock,\n Duration,\n Effect,\n Match,\n Option,\n Predicate,\n Redacted,\n Ref,\n Schema,\n Stream,\n} from \"effect\"\nimport { Base64 } from \"effect/encoding\"\nimport * as Socket from \"effect/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport * as WebSocketSession from \"@effect-uai/core/WebSocketSession\"\nimport { WebSocket as WSWebSocket } from \"ws\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Target latency knob (`target_streaming_delay_ms`); lower = faster, less accurate. */\n readonly targetStreamingDelay?: Duration.Duration\n /**\n * Silence gap after which the accumulated deltas are committed as a synthetic\n * `final`. Voxtral Realtime does no utterance segmentation, so this is how the\n * adapter delimits turns. Default 700 ms.\n */\n readonly utteranceSilence?: Duration.Duration\n /**\n * Build the socket yourself, for a proxy or an in-memory transport. The\n * default attaches the bearer token through the `ws` package.\n */\n readonly webSocket?: Socket.WebSocketConstructor[\"Service\"]\n}\n\nconst DEFAULT_SILENCE = Duration.millis(700)\n\n// ---------------------------------------------------------------------------\n// AudioFormat gate — Voxtral Realtime ingests pcm_s16le @ 16000 mono only.\n// ---------------------------------------------------------------------------\n\nconst ensureInputFormat = (format: AudioFormat): Effect.Effect<void, AiError.AiError> =>\n format.container === \"raw\" && format.encoding === \"pcm_s16le\" && format.sampleRate === 16000\n ? Effect.void\n : Effect.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"inputFormat\",\n reason: `Voxtral Realtime accepts pcm_s16le @ 16000 mono only. Got ${JSON.stringify(format)}.`,\n }),\n )\n\n// ---------------------------------------------------------------------------\n// URL + client frame builders\n// ---------------------------------------------------------------------------\n\nconst buildWsUrl = (cfg: Config, model: string) => {\n const base = (cfg.baseUrl ?? \"https://api.mistral.ai\").replace(/^http/, \"ws\")\n return `${base}/v1/audio/transcriptions/realtime?model=${encodeURIComponent(model)}`\n}\n\nconst sessionUpdateFrame = (cfg: Config) =>\n JSON.stringify({\n type: \"session.update\",\n session: {\n audio_format: { encoding: \"pcm_s16le\", sample_rate: 16000 },\n ...(cfg.targetStreamingDelay !== undefined && {\n target_streaming_delay_ms: Duration.toMillis(cfg.targetStreamingDelay),\n }),\n },\n })\n\nconst audioAppendFrame = (bytes: Uint8Array) =>\n JSON.stringify({ type: \"input_audio.append\", audio: Base64.encode(bytes) })\n\nconst audioEndFrame = JSON.stringify({ type: \"input_audio.end\" })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst ServerEvent = Schema.Union([\n Schema.Struct({ type: Schema.Literal(\"session.created\") }),\n Schema.Struct({ type: Schema.Literal(\"session.updated\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.language\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.segment\") }),\n Schema.Struct({ type: Schema.Literal(\"transcription.text.delta\"), text: Schema.String }),\n Schema.Struct({ type: Schema.Literal(\"transcription.done\"), text: Schema.String }),\n Schema.Struct({\n type: Schema.Literal(\"error\"),\n error: Schema.Struct({\n message: Schema.String,\n code: Schema.optional(Schema.NullOr(Schema.Number)),\n }),\n }),\n])\nconst decodeServerEvent = Schema.decodeUnknownEffect(ServerEvent)\n\n/** Mutable turn state shared by the frame handler and the silence finalizer. */\ntype TurnState = {\n readonly text: Ref.Ref<string>\n readonly lastActivityMs: Ref.Ref<number>\n}\n\nconst takeFinal = (\n state: TurnState,\n override?: string,\n): Effect.Effect<TranscriptEvent | undefined> =>\n Effect.gen(function* () {\n const acc = yield* Ref.getAndSet(state.text, \"\")\n const text = (override !== undefined && override.length > 0 ? override : acc).trim()\n return text.length > 0 ? { _tag: \"final\", text } : undefined\n })\n\nconst frameToEvent =\n (state: TurnState) =>\n (raw: string): Effect.Effect<TranscriptEvent | undefined> =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return undefined\n const decoded = yield* decodeServerEvent(json).pipe(Effect.option)\n if (Option.isNone(decoded)) return undefined\n return yield* Match.value(decoded.value).pipe(\n Match.when({ type: \"transcription.text.delta\" }, (m) =>\n Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const next = yield* Ref.updateAndGet(state.text, (t) => t + m.text)\n yield* Ref.set(state.lastActivityMs, now)\n // Cumulative partial so the UI shows the growing sentence.\n return { _tag: \"partial\", text: next } satisfies TranscriptEvent\n }),\n ),\n // End-of-audio: commit whatever's left as the final utterance.\n Match.when({ type: \"transcription.done\" }, (m) => takeFinal(state, m.text)),\n Match.when({ type: \"error\" }, (m) =>\n Effect.succeed<TranscriptEvent>({\n _tag: \"error\",\n ...(m.error.code != null && { code: String(m.error.code) }),\n message: m.error.message,\n }),\n ),\n // session.created / .updated / language / segment: no user-visible event.\n Match.orElse(() => Effect.succeed(undefined)),\n )\n })\n\n// Commits a synthetic final once the deltas go quiet; checks now, then every 150 ms.\nconst silenceFinals = (\n state: TurnState,\n silence: Duration.Duration,\n): Stream.Stream<TranscriptEvent> => {\n const silenceMs = Duration.toMillis(silence)\n return Stream.tick(\"150 millis\").pipe(\n Stream.mapEffect(() =>\n Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n const last = yield* Ref.get(state.lastActivityMs)\n const acc = yield* Ref.get(state.text)\n return acc.trim().length > 0 && now - last >= silenceMs\n ? yield* takeFinal(state)\n : undefined\n }),\n ),\n Stream.filter(Predicate.isNotUndefined),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n// ---------------------------------------------------------------------------\n\n// `@types/ws`'s WebSocket extends Node's EventEmitter while\n// `globalThis.WebSocket` extends EventTarget; the browser-style surface\n// Effect's Socket reads (`addEventListener` / `send` / `close`) is identical at\n// runtime, hence the single contained cast.\nconst authedWsConstructor =\n (cfg: Config): Socket.WebSocketConstructor[\"Service\"] =>\n (url) =>\n new WSWebSocket(url, undefined, {\n headers: { Authorization: `Bearer ${Redacted.value(cfg.apiKey)}` },\n }) as unknown as globalThis.WebSocket\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* ensureInputFormat(request.inputFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request.model)).pipe(\n Effect.provideService(\n Socket.WebSocketConstructor,\n cfg.webSocket ?? authedWsConstructor(cfg),\n ),\n )\n const state: TurnState = {\n text: yield* Ref.make(\"\"),\n lastActivityMs: yield* Ref.make(0),\n }\n const { write } = yield* socket.writer\n const asAiError = WebSocketSession.toAiError(\"mistral\")\n\n const frames = Stream.fromPull(Socket.readerString(socket)).pipe(\n Stream.scoped,\n Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty),\n Stream.mapError(asAiError),\n Stream.mapEffect(frameToEvent(state)),\n Stream.filter(Predicate.isNotUndefined),\n )\n const events = Stream.merge(\n frames,\n silenceFinals(state, cfg.utteranceSilence ?? DEFAULT_SILENCE),\n { haltStrategy: \"left\" },\n )\n\n // session.update first, then base64 PCM, then signal end. Writes wait for the reader to connect.\n const outgoing = Stream.concat(\n Stream.concat(\n Stream.make(sessionUpdateFrame(cfg)),\n Stream.map(audioIn, audioAppendFrame),\n ),\n Stream.make(audioEndFrame),\n ).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)))\n\n // The server's close ends the transcript; the audio running out does not.\n return Stream.merge(events, Stream.drain(outgoing), { haltStrategy: \"left\" })\n }),\n )\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,317 @@
1
+ import { streamTranscription } from "./realtimeStt.mjs";
2
+ import { a as beforeEach, i as beforeAll, n as globalExpect, o as describe, r as afterAll, s as it$2, t as TestRunner } from "./index.C4tKZ0yW-CUBm3Bmz.mjs";
3
+ import { Effect, Exit, Fiber, Redacted, Stream } from "effect";
4
+ import * as Arbitrary from "effect/Arbitrary";
5
+ import * as Cause from "effect/Cause";
6
+ import * as Duration$1 from "effect/Duration";
7
+ import * as Effect$1 from "effect/Effect";
8
+ import * as Exit$1 from "effect/Exit";
9
+ import "effect/Fiber";
10
+ import { constVoid, flow, pipe } from "effect/Function";
11
+ import * as Layer$1 from "effect/Layer";
12
+ import * as Schedule from "effect/Schedule";
13
+ import * as Scope from "effect/Scope";
14
+ import * as TestClock$1 from "effect/testing/TestClock";
15
+ import * as TestConsole from "effect/testing/TestConsole";
16
+ import { TestClock } from "effect/testing";
17
+ import * as FakeWebSocket from "@effect-uai/core/testing/FakeWebSocket";
18
+ //#region ../../../node_modules/.pnpm/@effect+vitest@4.1.2_effect@4.1.2_vitest@5.0.3_@types+node@26.2.0_vite@7.3.2_@types+nod_a2fffe0deec250ef9d6207b86a9a1ab8/node_modules/@effect/vitest/dist/internal/internal.js
19
+ /**
20
+ * @since 4.0.0
21
+ */
22
+ const getCurrentSuite = TestRunner.getCurrentSuite;
23
+ const runPromise = (effect, ctx) => Effect$1.runPromise(Effect$1.uninterruptibleMask((restore) => Effect$1.gen(function* () {
24
+ const exit = yield* Effect$1.exit(restore(effect));
25
+ if (Exit$1.isFailure(exit)) {
26
+ const errors = Cause.prettyErrors(exit.cause);
27
+ for (let i = 0; i < errors.length; i++) yield* Effect$1.logError(errors[i]);
28
+ }
29
+ return yield* exit;
30
+ })), { signal: ctx?.signal });
31
+ const settleOnAbort = (ctx, promise) => {
32
+ const onAbort = () => ctx.onTestFinished(() => promise.then(constVoid, constVoid));
33
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
34
+ const cleanup = () => ctx.signal.removeEventListener("abort", onAbort);
35
+ promise.then(cleanup, cleanup);
36
+ if (ctx.signal.aborted) onAbort();
37
+ return promise;
38
+ };
39
+ /** @internal */
40
+ const runTest = (ctx) => (effect) => {
41
+ const promise = runPromise(effect, ctx);
42
+ return ctx ? settleOnAbort(ctx, promise) : promise;
43
+ };
44
+ const TestEnv = /*#__PURE__*/ Layer$1.mergeAll(TestConsole.layer, /*#__PURE__*/ TestClock$1.layer());
45
+ /** @internal */
46
+ const testOptions = (timeout) => typeof timeout === "number" ? { timeout } : timeout ?? {};
47
+ const hookTimeout = (timeout) => timeout === void 0 ? void 0 : Duration$1.toMillis(Duration$1.fromInputUnsafe(timeout));
48
+ const propertyTestOptions = (timeout) => typeof timeout === "number" ? void 0 : timeout;
49
+ const checkOptions = (timeout) => propertyTestOptions(timeout)?.arbitrary;
50
+ const compileArbitraryInput = (input) => Arbitrary.isArbitrary(input) ? input : Arbitrary.schema(input);
51
+ const makeArbitrary = (arbitraries) => Arbitrary.all(Array.isArray(arbitraries) ? arbitraries.map(compileArbitraryInput) : Object.fromEntries(Object.entries(arbitraries).map(([key, input]) => [key, compileArbitraryInput(input)])));
52
+ const normalizeProperty = (property, value) => Effect$1.catchCause(Effect$1.suspend(() => {
53
+ const output = property(value);
54
+ return Effect$1.isEffect(output) ? output : Effect$1.succeed(output);
55
+ }), (cause) => Cause.hasInterrupts(cause) ? Effect$1.failCause(cause) : Effect$1.fail(cause));
56
+ const runCheck = (ctx, arbitrary, property, options) => runTest(ctx)(Effect$1.flatMapEager(Arbitrary.checkEffect(arbitrary, (value) => normalizeProperty(property, value), options), (result) => {
57
+ const failure = Arbitrary.formatCheckFailure(result);
58
+ return failure === void 0 ? Effect$1.void : Effect$1.die(new Error(failure));
59
+ }));
60
+ const withFixturesOf = (source, f) => Object.defineProperty(f, "toString", { value: () => source.toString() });
61
+ const makeItProxy = (it, overrides) => new Proxy(it, {
62
+ apply(target, thisArg, argArray) {
63
+ return Reflect.apply(target, thisArg, argArray);
64
+ },
65
+ get(target, property, receiver) {
66
+ if (Object.hasOwn(overrides, property)) return Reflect.get(overrides, property);
67
+ return Reflect.get(target, property, receiver);
68
+ }
69
+ });
70
+ const collectTasks = (tasks, acc = []) => {
71
+ for (let i = 0; i < tasks.length; i++) {
72
+ const task = tasks[i];
73
+ if (task.type === "test" && task.mode !== "skip" && task.mode !== "todo") acc.push(task);
74
+ else if (task.tasks !== void 0) collectTasks(task.tasks, acc);
75
+ }
76
+ return acc;
77
+ };
78
+ const runsMoreThanOnce = (task) => {
79
+ return ((typeof task.retry === "number" ? task.retry : task.retry?.count) ?? 0) > 0 || (task.repeats ?? 0) > 0;
80
+ };
81
+ const registerProp = (it, name, arbitraries, property, timeout) => {
82
+ const arbitrary = makeArbitrary(arbitraries);
83
+ it(name, testOptions(timeout), withFixturesOf(() => {}, (ctx) => runCheck(ctx, arbitrary, (values) => property(values, ctx), checkOptions(timeout))));
84
+ };
85
+ const makeTester = (mapEffect, it) => {
86
+ const run = (ctx, args, self) => pipe(Effect$1.suspend(() => self(...args)), mapEffect, runTest(ctx));
87
+ const register = (test) => (name, self, timeout) => test(name, testOptions(timeout), withFixturesOf(self, (ctx) => run(ctx, [ctx], self)));
88
+ const each = (cases) => (name, self, timeout) => it.for(cases)(name, testOptions(timeout), withFixturesOf(self, (args, ctx) => run(ctx, [args, ctx], self).then(constVoid)));
89
+ const prop = (name, arbitraries, self, timeout) => registerProp(it, name, arbitraries, (values, ctx) => Effect$1.mapEager(mapEffect(Effect$1.suspend(() => self(values, ctx))), (value) => value !== false), timeout);
90
+ return Object.assign(register(it), {
91
+ skip: register(it.skip),
92
+ skipIf: (condition) => register(it.skipIf(condition)),
93
+ runIf: (condition) => register(it.runIf(condition)),
94
+ only: register(it.only),
95
+ each,
96
+ fails: register(it.fails),
97
+ prop
98
+ });
99
+ };
100
+ const makeProp = (it) => (name, arbitraries, self, timeout) => registerProp(it, name, arbitraries, (values, ctx) => {
101
+ const result = self(values, ctx);
102
+ if (result !== void 0 && typeof result !== "boolean") throw new TypeError("it.prop callbacks must return boolean or void. Use it.effect.prop for Effect callbacks.");
103
+ return result !== false;
104
+ }, timeout);
105
+ const makeLayer = (it) => (layer_, options) => (...args) => {
106
+ const excludeTestServices = options?.excludeTestServices ?? false;
107
+ const withTestEnv = excludeTestServices ? layer_ : Layer$1.provideMerge(layer_, TestEnv);
108
+ const memoMap = options?.memoMap ?? Effect$1.runSync(Layer$1.makeMemoMap);
109
+ const buildPerTest = options?.buildPerTest ?? false;
110
+ const suiteOptions = options?.concurrent === void 0 ? {} : { concurrent: options.concurrent };
111
+ const makeBlockMethods = (provide) => makeItProxy(it, {
112
+ effect: makeTester((effect) => provide(Effect$1.scoped(effect)), it),
113
+ prop: makeProp(it),
114
+ flakyTest,
115
+ layer(nestedLayer, nestedOptions) {
116
+ return makeLayer(it)(Layer$1.provideMerge(nestedLayer, withTestEnv), {
117
+ ...nestedOptions,
118
+ memoMap: Layer$1.forkMemoMapUnsafe(memoMap),
119
+ excludeTestServices,
120
+ buildPerTest: buildPerTest || nestedOptions?.buildPerTest === true
121
+ });
122
+ }
123
+ });
124
+ if (buildPerTest) {
125
+ const methods = makeBlockMethods((effect) => Effect$1.scopedWith((scope) => Layer$1.buildWithMemoMap(withTestEnv, Layer$1.forkMemoMapUnsafe(memoMap), scope).pipe(Effect$1.orDie, Effect$1.flatMap((context) => Effect$1.provide(effect, context)))));
126
+ if (args.length === 1) return args[0](methods);
127
+ return describe(args[0], suiteOptions, () => args[1](methods));
128
+ }
129
+ const scope = Effect$1.runSync(Scope.make());
130
+ const contextEffect = Layer$1.buildWithMemoMap(withTestEnv, memoMap, scope).pipe(Effect$1.orDie, Effect$1.cached, Effect$1.runSync);
131
+ let closed = false;
132
+ const closeScope = () => {
133
+ if (closed) return Promise.resolve();
134
+ closed = true;
135
+ return runPromise(Scope.close(scope, Exit$1.void));
136
+ };
137
+ const methods = makeBlockMethods((effect) => Effect$1.flatMap(contextEffect, (context) => Effect$1.provide(effect, context)));
138
+ if (args.length === 1) {
139
+ const currentSuite = getCurrentSuite();
140
+ const previousTasks = new Set(currentSuite.tasks);
141
+ args[0](methods);
142
+ const blockTasks = collectTasks(currentSuite.tasks.filter((task) => !previousTasks.has(task)));
143
+ if (blockTasks.length === 0) {
144
+ afterAll(() => closeScope(), hookTimeout(options?.timeout));
145
+ return;
146
+ }
147
+ const blockTaskSet = new Set(blockTasks);
148
+ const releaseAfterLastTest = !blockTasks.some(runsMoreThanOnce);
149
+ let remaining = blockTasks.length;
150
+ beforeEach(({ onTestFinished, signal, task }) => {
151
+ if (!blockTaskSet.has(task)) return;
152
+ if (releaseAfterLastTest) onTestFinished(() => {
153
+ remaining--;
154
+ if (remaining === 0) return closeScope();
155
+ });
156
+ return runPromise(Effect$1.asVoid(contextEffect), { signal });
157
+ }, hookTimeout(options?.timeout));
158
+ afterAll(() => closeScope(), hookTimeout(options?.timeout));
159
+ return;
160
+ }
161
+ return describe(args[0], suiteOptions, () => {
162
+ beforeAll(() => runPromise(Effect$1.asVoid(contextEffect)), hookTimeout(options?.timeout));
163
+ afterAll(() => closeScope(), hookTimeout(options?.timeout));
164
+ return args[1](methods);
165
+ });
166
+ };
167
+ /** @internal */
168
+ const flakyTest = (self, timeout = Duration$1.seconds(30)) => pipe(self, Effect$1.scoped, Effect$1.sandbox, Effect$1.retry(pipe(Schedule.recurs(10), Schedule.while((_) => Effect$1.succeed(Duration$1.isLessThanOrEqualTo(Duration$1.fromInputUnsafe(_.elapsed), Duration$1.fromInputUnsafe(timeout)))))), Effect$1.orDie);
169
+ /** @internal */
170
+ const makeMethods = (it) => makeItProxy(it, {
171
+ effect: makeTester(flow(Effect$1.scoped, Effect$1.provide(TestEnv)), it),
172
+ live: makeTester(Effect$1.scoped, it),
173
+ flakyTest,
174
+ layer: makeLayer(it),
175
+ prop: makeProp(it)
176
+ });
177
+ /** @internal */
178
+ const it$1 = /*#__PURE__*/ makeMethods(it$2);
179
+ /** @internal */
180
+ const { effect, layer, live, prop } = it$1;
181
+ //#endregion
182
+ //#region ../../../node_modules/.pnpm/@effect+vitest@4.1.2_effect@4.1.2_vitest@5.0.3_@types+node@26.2.0_vite@7.3.2_@types+nod_a2fffe0deec250ef9d6207b86a9a1ab8/node_modules/@effect/vitest/dist/index.js
183
+ /**
184
+ * Vitest test API extended with Effect test helpers.
185
+ *
186
+ * @stability unstable
187
+ * @category testing
188
+ * @since 4.0.0
189
+ */
190
+ const it = it$1;
191
+ //#endregion
192
+ //#region src/realtimeStt.test.ts
193
+ const request = {
194
+ model: "voxtral-mini-transcribe-realtime-2602",
195
+ inputFormat: {
196
+ container: "raw",
197
+ encoding: "pcm_s16le",
198
+ sampleRate: 16e3,
199
+ channels: 1
200
+ }
201
+ };
202
+ const sessionCreated = {
203
+ type: "session.created",
204
+ session: {
205
+ request_id: "req_1",
206
+ model: request.model
207
+ }
208
+ };
209
+ const delta = (text) => ({
210
+ type: "transcription.text.delta",
211
+ text
212
+ });
213
+ const start = (server, audioIn) => streamTranscription({
214
+ apiKey: Redacted.make("test-key"),
215
+ webSocket: server.connect
216
+ })(audioIn, request).pipe(Stream.runCollect, Effect.exit, Effect.forkChild);
217
+ /** Run a transcription against `server`, closing it, if asked, once the frames are in. */
218
+ const transcribe = (server, audioIn, close) => Effect.gen(function* () {
219
+ const collector = yield* start(server, audioIn);
220
+ if (close !== void 0) {
221
+ yield* server.greeted;
222
+ yield* server.awaitSent(close.afterSent ?? 0);
223
+ yield* server.close(close.code);
224
+ }
225
+ return yield* Fiber.join(collector);
226
+ });
227
+ describe("Mistral Realtime STT socket", () => {
228
+ it.live("configures the session before any audio, and ends on a clean close", () => Effect.gen(function* () {
229
+ const server = yield* FakeWebSocket.make({ greeting: [
230
+ sessionCreated,
231
+ delta("hel"),
232
+ delta("lo"),
233
+ {
234
+ type: "transcription.done",
235
+ text: "hello",
236
+ model: request.model,
237
+ segments: []
238
+ },
239
+ {
240
+ type: "error",
241
+ error: {
242
+ message: "slow down",
243
+ code: 3001
244
+ }
245
+ }
246
+ ] });
247
+ const exit = yield* transcribe(server, Stream.make(new Uint8Array([1, 2])), {
248
+ code: 1e3,
249
+ afterSent: 3
250
+ });
251
+ globalExpect(exit).toEqual(Exit.succeed([
252
+ {
253
+ _tag: "partial",
254
+ text: "hel"
255
+ },
256
+ {
257
+ _tag: "partial",
258
+ text: "hello"
259
+ },
260
+ {
261
+ _tag: "final",
262
+ text: "hello"
263
+ },
264
+ {
265
+ _tag: "error",
266
+ code: "3001",
267
+ message: "slow down"
268
+ }
269
+ ]));
270
+ const sent = yield* server.sent;
271
+ globalExpect(sent.map((f) => f.type)).toEqual([
272
+ "session.update",
273
+ "input_audio.append",
274
+ "input_audio.end"
275
+ ]);
276
+ }).pipe(Effect.scoped));
277
+ it.live("fails rather than ending when the connection drops", () => Effect.gen(function* () {
278
+ const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] });
279
+ const exit = yield* transcribe(server, Stream.never, { code: 1011 });
280
+ globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("Unavailable");
281
+ }).pipe(Effect.scoped));
282
+ it.live("fails with the audio input's own error", () => Effect.gen(function* () {
283
+ const server = yield* FakeWebSocket.make({ greeting: [sessionCreated] });
284
+ const exit = yield* transcribe(server, Stream.fail("microphone unplugged"));
285
+ globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("microphone unplugged");
286
+ }).pipe(Effect.scoped));
287
+ it.effect("commits a synthetic final once the deltas go quiet", () => Effect.gen(function* () {
288
+ const server = yield* FakeWebSocket.make({ greeting: [
289
+ sessionCreated,
290
+ delta("hel"),
291
+ delta("lo ")
292
+ ] });
293
+ const collector = yield* start(server, Stream.never);
294
+ yield* server.greeted;
295
+ yield* TestClock.withLive(Effect.sleep("5 millis"));
296
+ yield* TestClock.adjust("1 second");
297
+ yield* server.close(1e3);
298
+ globalExpect(yield* Fiber.join(collector)).toEqual(Exit.succeed([
299
+ {
300
+ _tag: "partial",
301
+ text: "hel"
302
+ },
303
+ {
304
+ _tag: "partial",
305
+ text: "hello "
306
+ },
307
+ {
308
+ _tag: "final",
309
+ text: "hello"
310
+ }
311
+ ]));
312
+ }).pipe(Effect.scoped));
313
+ });
314
+ //#endregion
315
+ export {};
316
+
317
+ //# sourceMappingURL=realtimeStt.test.mjs.map