@effect-uai/core 0.2.0 → 0.4.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.
Files changed (209) hide show
  1. package/README.md +1 -1
  2. package/dist/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
  3. package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
  4. package/dist/Audio-BfCTGnH3.d.mts +61 -0
  5. package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
  6. package/dist/Image-DxyXqzAM.d.mts +61 -0
  7. package/dist/Image-DxyXqzAM.d.mts.map +1 -0
  8. package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
  9. package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
  10. package/dist/Media-D_CpcM1Z.d.mts +57 -0
  11. package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
  12. package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
  13. package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
  14. package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
  15. package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
  16. package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
  17. package/dist/Turn-7geUcKsf.d.mts.map +1 -0
  18. package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
  19. package/dist/dist-DV5ISja1.mjs +13782 -0
  20. package/dist/dist-DV5ISja1.mjs.map +1 -0
  21. package/dist/domain/AiError.d.mts +2 -2
  22. package/dist/domain/AiError.mjs +19 -3
  23. package/dist/domain/AiError.mjs.map +1 -1
  24. package/dist/domain/Audio.d.mts +2 -0
  25. package/dist/domain/Audio.mjs +14 -0
  26. package/dist/domain/Audio.mjs.map +1 -0
  27. package/dist/domain/Image.d.mts +2 -0
  28. package/dist/domain/Image.mjs +58 -0
  29. package/dist/domain/Image.mjs.map +1 -0
  30. package/dist/domain/Items.d.mts +2 -2
  31. package/dist/domain/Items.mjs +19 -42
  32. package/dist/domain/Items.mjs.map +1 -1
  33. package/dist/domain/Media.d.mts +2 -0
  34. package/dist/domain/Media.mjs +14 -0
  35. package/dist/domain/Media.mjs.map +1 -0
  36. package/dist/domain/Music.d.mts +116 -0
  37. package/dist/domain/Music.d.mts.map +1 -0
  38. package/dist/domain/Music.mjs +29 -0
  39. package/dist/domain/Music.mjs.map +1 -0
  40. package/dist/domain/Transcript.d.mts +95 -0
  41. package/dist/domain/Transcript.d.mts.map +1 -0
  42. package/dist/domain/Transcript.mjs +22 -0
  43. package/dist/domain/Transcript.mjs.map +1 -0
  44. package/dist/domain/Turn.d.mts +1 -1
  45. package/dist/domain/Turn.mjs +1 -1
  46. package/dist/embedding-model/Embedding.d.mts +107 -0
  47. package/dist/embedding-model/Embedding.d.mts.map +1 -0
  48. package/dist/embedding-model/Embedding.mjs +18 -0
  49. package/dist/embedding-model/Embedding.mjs.map +1 -0
  50. package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
  51. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
  52. package/dist/embedding-model/EmbeddingModel.mjs +17 -0
  53. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
  54. package/dist/index.d.mts +21 -7
  55. package/dist/index.mjs +16 -2
  56. package/dist/language-model/LanguageModel.d.mts +12 -20
  57. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  58. package/dist/language-model/LanguageModel.mjs +3 -20
  59. package/dist/language-model/LanguageModel.mjs.map +1 -1
  60. package/dist/loop/Loop.d.mts +31 -7
  61. package/dist/loop/Loop.d.mts.map +1 -1
  62. package/dist/loop/Loop.mjs +39 -6
  63. package/dist/loop/Loop.mjs.map +1 -1
  64. package/dist/loop/Loop.test.d.mts +1 -0
  65. package/dist/loop/Loop.test.mjs +411 -0
  66. package/dist/loop/Loop.test.mjs.map +1 -0
  67. package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
  68. package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
  69. package/dist/math/Vector.d.mts +47 -0
  70. package/dist/math/Vector.d.mts.map +1 -0
  71. package/dist/math/Vector.mjs +117 -0
  72. package/dist/math/Vector.mjs.map +1 -0
  73. package/dist/music-generator/MusicGenerator.d.mts +77 -0
  74. package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
  75. package/dist/music-generator/MusicGenerator.mjs +51 -0
  76. package/dist/music-generator/MusicGenerator.mjs.map +1 -0
  77. package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
  78. package/dist/music-generator/MusicGenerator.test.mjs +154 -0
  79. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
  80. package/dist/observability/Metrics.d.mts +2 -2
  81. package/dist/observability/Metrics.d.mts.map +1 -1
  82. package/dist/observability/Metrics.mjs +1 -1
  83. package/dist/observability/Metrics.mjs.map +1 -1
  84. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
  85. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
  86. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
  87. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
  88. package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
  89. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
  90. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
  91. package/dist/streaming/JSONL.d.mts +10 -3
  92. package/dist/streaming/JSONL.d.mts.map +1 -1
  93. package/dist/streaming/JSONL.mjs +13 -2
  94. package/dist/streaming/JSONL.mjs.map +1 -1
  95. package/dist/streaming/JSONL.test.d.mts +1 -0
  96. package/dist/streaming/JSONL.test.mjs +70 -0
  97. package/dist/streaming/JSONL.test.mjs.map +1 -0
  98. package/dist/streaming/Lines.mjs +1 -1
  99. package/dist/streaming/SSE.d.mts +2 -2
  100. package/dist/streaming/SSE.d.mts.map +1 -1
  101. package/dist/streaming/SSE.mjs +1 -1
  102. package/dist/streaming/SSE.mjs.map +1 -1
  103. package/dist/streaming/SSE.test.d.mts +1 -0
  104. package/dist/streaming/SSE.test.mjs +72 -0
  105. package/dist/streaming/SSE.test.mjs.map +1 -0
  106. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  107. package/dist/structured-format/StructuredFormat.mjs +1 -1
  108. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  109. package/dist/testing/MockMusicGenerator.d.mts +39 -0
  110. package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
  111. package/dist/testing/MockMusicGenerator.mjs +96 -0
  112. package/dist/testing/MockMusicGenerator.mjs.map +1 -0
  113. package/dist/testing/MockProvider.d.mts +6 -6
  114. package/dist/testing/MockProvider.d.mts.map +1 -1
  115. package/dist/testing/MockProvider.mjs.map +1 -1
  116. package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
  117. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
  118. package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
  119. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
  120. package/dist/testing/MockTranscriber.d.mts +37 -0
  121. package/dist/testing/MockTranscriber.d.mts.map +1 -0
  122. package/dist/testing/MockTranscriber.mjs +77 -0
  123. package/dist/testing/MockTranscriber.mjs.map +1 -0
  124. package/dist/tool/HistoryCheck.d.mts +6 -3
  125. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  126. package/dist/tool/HistoryCheck.mjs +7 -1
  127. package/dist/tool/HistoryCheck.mjs.map +1 -1
  128. package/dist/tool/Outcome.d.mts +138 -2
  129. package/dist/tool/Outcome.d.mts.map +1 -0
  130. package/dist/tool/Outcome.mjs +32 -10
  131. package/dist/tool/Outcome.mjs.map +1 -1
  132. package/dist/tool/Resolvers.d.mts +11 -8
  133. package/dist/tool/Resolvers.d.mts.map +1 -1
  134. package/dist/tool/Resolvers.mjs +10 -1
  135. package/dist/tool/Resolvers.mjs.map +1 -1
  136. package/dist/tool/Resolvers.test.d.mts +1 -0
  137. package/dist/tool/Resolvers.test.mjs +317 -0
  138. package/dist/tool/Resolvers.test.mjs.map +1 -0
  139. package/dist/tool/Tool.d.mts +1 -1
  140. package/dist/tool/Tool.mjs +1 -1
  141. package/dist/tool/Tool.mjs.map +1 -1
  142. package/dist/tool/ToolEvent.d.mts +151 -2
  143. package/dist/tool/ToolEvent.d.mts.map +1 -0
  144. package/dist/tool/ToolEvent.mjs +30 -4
  145. package/dist/tool/ToolEvent.mjs.map +1 -1
  146. package/dist/tool/Toolkit.d.mts +19 -10
  147. package/dist/tool/Toolkit.d.mts.map +1 -1
  148. package/dist/tool/Toolkit.mjs +5 -5
  149. package/dist/tool/Toolkit.mjs.map +1 -1
  150. package/dist/tool/Toolkit.test.d.mts +1 -0
  151. package/dist/tool/Toolkit.test.mjs +113 -0
  152. package/dist/tool/Toolkit.test.mjs.map +1 -0
  153. package/dist/transcriber/Transcriber.d.mts +101 -0
  154. package/dist/transcriber/Transcriber.d.mts.map +1 -0
  155. package/dist/transcriber/Transcriber.mjs +49 -0
  156. package/dist/transcriber/Transcriber.mjs.map +1 -0
  157. package/dist/transcriber/Transcriber.test.d.mts +1 -0
  158. package/dist/transcriber/Transcriber.test.mjs +130 -0
  159. package/dist/transcriber/Transcriber.test.mjs.map +1 -0
  160. package/package.json +65 -13
  161. package/src/domain/AiError.ts +21 -0
  162. package/src/domain/Audio.ts +88 -0
  163. package/src/domain/Image.ts +75 -0
  164. package/src/domain/Items.ts +18 -47
  165. package/src/domain/Media.ts +61 -0
  166. package/src/domain/Music.ts +121 -0
  167. package/src/domain/Transcript.ts +83 -0
  168. package/src/embedding-model/Embedding.ts +117 -0
  169. package/src/embedding-model/EmbeddingModel.ts +107 -0
  170. package/src/index.ts +15 -1
  171. package/src/language-model/LanguageModel.ts +2 -22
  172. package/src/loop/Loop.test.ts +114 -2
  173. package/src/loop/Loop.ts +69 -5
  174. package/src/math/Vector.ts +138 -0
  175. package/src/music-generator/MusicGenerator.test.ts +170 -0
  176. package/src/music-generator/MusicGenerator.ts +123 -0
  177. package/src/observability/Metrics.ts +1 -1
  178. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
  179. package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
  180. package/src/streaming/JSONL.ts +12 -0
  181. package/src/streaming/SSE.ts +1 -1
  182. package/src/structured-format/StructuredFormat.ts +2 -2
  183. package/src/testing/MockMusicGenerator.ts +170 -0
  184. package/src/testing/MockProvider.ts +2 -2
  185. package/src/testing/MockSpeechSynthesizer.ts +165 -0
  186. package/src/testing/MockTranscriber.ts +139 -0
  187. package/src/tool/HistoryCheck.ts +2 -5
  188. package/src/tool/Outcome.ts +36 -36
  189. package/src/tool/Resolvers.test.ts +11 -35
  190. package/src/tool/Resolvers.ts +5 -14
  191. package/src/tool/Tool.ts +9 -9
  192. package/src/tool/ToolEvent.ts +28 -24
  193. package/src/tool/Toolkit.test.ts +97 -2
  194. package/src/tool/Toolkit.ts +57 -33
  195. package/src/transcriber/Transcriber.test.ts +125 -0
  196. package/src/transcriber/Transcriber.ts +127 -0
  197. package/dist/Items-D1C2686t.d.mts.map +0 -1
  198. package/dist/Outcome-GiaNvt7i.d.mts +0 -32
  199. package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
  200. package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
  201. package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
  202. package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
  203. package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
  204. package/dist/Turn-Bi83du4I.d.mts.map +0 -1
  205. package/dist/match/Match.d.mts +0 -16
  206. package/dist/match/Match.d.mts.map +0 -1
  207. package/dist/match/Match.mjs +0 -15
  208. package/dist/match/Match.mjs.map +0 -1
  209. package/src/match/Match.ts +0 -9
@@ -0,0 +1,154 @@
1
+ import { configInput, promptsInput } from "../domain/Music.mjs";
2
+ import { generate, streamGeneration, streamGenerationFrom } from "./MusicGenerator.mjs";
3
+ import { i as it, n as globalExpect, r as describe, t as import_dist } from "../dist-DV5ISja1.mjs";
4
+ import { layer, layerWithoutInteractive } from "../testing/MockMusicGenerator.mjs";
5
+ import { Effect, Stream } from "effect";
6
+ //#region src/music-generator/MusicGenerator.test.ts
7
+ const result = {
8
+ format: {
9
+ container: "mp3",
10
+ encoding: "mp3",
11
+ sampleRate: 44100,
12
+ channels: 2
13
+ },
14
+ bytes: new Uint8Array([
15
+ 255,
16
+ 251,
17
+ 144,
18
+ 0
19
+ ]),
20
+ durationSeconds: 30,
21
+ lyrics: "[Verse]\nhello\n",
22
+ watermark: { kind: "synthid" }
23
+ };
24
+ const chunk = (n) => ({ bytes: new Uint8Array([n]) });
25
+ describe("MusicGenerator.generate", () => {
26
+ it("returns the scripted MusicResult", async () => {
27
+ const mock = layer({ results: [result] });
28
+ const program = generate({
29
+ model: "mock-music",
30
+ prompts: "upbeat indie pop"
31
+ });
32
+ const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
33
+ globalExpect(out.bytes).toEqual(result.bytes);
34
+ globalExpect(out.durationSeconds).toBe(30);
35
+ globalExpect(out.watermark?.kind).toBe("synthid");
36
+ globalExpect(out.lyrics).toContain("[Verse]");
37
+ });
38
+ it("records the request shape on the recorder", async () => {
39
+ const mock = layer({ results: [result, result] });
40
+ const program = Effect.gen(function* () {
41
+ yield* generate({
42
+ model: "m",
43
+ prompts: "techno"
44
+ });
45
+ yield* generate({
46
+ model: "m",
47
+ prompts: [{
48
+ text: "synthwave",
49
+ weight: 1
50
+ }, {
51
+ text: "80s movie OST",
52
+ weight: .4
53
+ }],
54
+ bpm: 120,
55
+ instrumental: true
56
+ });
57
+ return yield* mock.recorder;
58
+ });
59
+ const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
60
+ globalExpect(rec.generateCalls.length).toBe(2);
61
+ globalExpect(rec.generateCalls[1].bpm).toBe(120);
62
+ globalExpect(rec.generateCalls[1].instrumental).toBe(true);
63
+ globalExpect(Array.isArray(rec.generateCalls[1].prompts)).toBe(true);
64
+ });
65
+ });
66
+ describe("MusicGenerator.streamGeneration", () => {
67
+ it("emits scripted chunks", async () => {
68
+ const mock = layer({ streamGenerationChunks: [[
69
+ chunk(1),
70
+ chunk(2),
71
+ chunk(3)
72
+ ]] });
73
+ const program = Stream.runCollect(streamGeneration({
74
+ model: "m",
75
+ prompts: "ambient"
76
+ }));
77
+ globalExpect((await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))).map((c) => Array.from(c.bytes))).toEqual([
78
+ [1],
79
+ [2],
80
+ [3]
81
+ ]);
82
+ });
83
+ });
84
+ describe("MusicGenerator capability marker (compile-time)", () => {
85
+ const sgfReq = {
86
+ model: "m",
87
+ prompts: ""
88
+ };
89
+ it("requires `MusicInteractiveSession` on the R channel of streamGenerationFrom", () => {
90
+ (0, import_dist.expectTypeOf)(Stream.fromIterable([promptsInput([{ text: "techno" }])]).pipe(streamGenerationFrom(sgfReq))).toEqualTypeOf();
91
+ });
92
+ it("does NOT require `MusicInteractiveSession` for sync `generate`", () => {
93
+ (0, import_dist.expectTypeOf)(generate({
94
+ model: "m",
95
+ prompts: "ambient"
96
+ })).toEqualTypeOf();
97
+ });
98
+ it("does NOT require `MusicInteractiveSession` for `streamGeneration`", () => {
99
+ (0, import_dist.expectTypeOf)(streamGeneration({
100
+ model: "m",
101
+ prompts: "ambient"
102
+ })).toEqualTypeOf();
103
+ });
104
+ it("a layer without the marker leaves `MusicInteractiveSession` unsatisfied in R", () => {
105
+ const noMarker = layerWithoutInteractive({});
106
+ const audio = Stream.fromIterable([promptsInput([{ text: "techno" }])]).pipe(streamGenerationFrom(sgfReq));
107
+ (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(noMarker.layer))).toEqualTypeOf();
108
+ });
109
+ it("a full layer (with marker) clears R to never", () => {
110
+ const fullMock = layer({ streamGenerationFromChunks: [[]] });
111
+ const audio = Stream.fromIterable([promptsInput([{ text: "techno" }])]).pipe(streamGenerationFrom(sgfReq));
112
+ (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))).toEqualTypeOf();
113
+ });
114
+ });
115
+ describe("MusicGenerator.streamGenerationFrom", () => {
116
+ const sgfReq = {
117
+ model: "lyria-realtime-001",
118
+ prompts: ""
119
+ };
120
+ it("drains a session-input stream and emits scripted audio", async () => {
121
+ const mock = layer({ streamGenerationFromChunks: [[chunk(10), chunk(20)]] });
122
+ const audio = Stream.fromIterable([
123
+ promptsInput([{
124
+ text: "minimal techno",
125
+ weight: 1
126
+ }]),
127
+ configInput({ bpm: 124 }),
128
+ promptsInput([{
129
+ text: "minimal techno",
130
+ weight: 1
131
+ }, {
132
+ text: "1980s synthwave",
133
+ weight: .3
134
+ }])
135
+ ]).pipe(streamGenerationFrom(sgfReq));
136
+ globalExpect((await Effect.runPromise(Stream.runCollect(audio).pipe(Effect.provide(mock.layer)))).map((c) => Array.from(c.bytes))).toEqual([[10], [20]]);
137
+ });
138
+ it("records the request on the streamGenerationFrom call channel", async () => {
139
+ const mock = layer({ streamGenerationFromChunks: [[chunk(42)]] });
140
+ const program = Effect.gen(function* () {
141
+ yield* Stream.runDrain(Stream.fromIterable([promptsInput([{ text: "x" }])]).pipe(streamGenerationFrom(sgfReq)));
142
+ return yield* mock.recorder;
143
+ });
144
+ const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
145
+ globalExpect(rec.streamGenerationFromCalls.length).toBe(1);
146
+ globalExpect(rec.streamGenerationFromCalls[0].model).toBe("lyria-realtime-001");
147
+ globalExpect(rec.generateCalls.length).toBe(0);
148
+ globalExpect(rec.streamGenerationCalls.length).toBe(0);
149
+ });
150
+ });
151
+ //#endregion
152
+ export {};
153
+
154
+ //# sourceMappingURL=MusicGenerator.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MusicGenerator.test.mjs","names":["MockMusicGenerator.layer","MusicGenerator.generate","MusicGenerator.streamGeneration","MusicGenerator.streamGenerationFrom","MockMusicGenerator.layerWithoutInteractive"],"sources":["../../src/music-generator/MusicGenerator.test.ts"],"sourcesContent":["import { Effect, Stream } from \"effect\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"../domain/AiError.js\"\nimport type { AudioChunk, AudioFormat } from \"../domain/Audio.js\"\nimport { configInput, promptsInput, type MusicResult } from \"../domain/Music.js\"\nimport * as MockMusicGenerator from \"../testing/MockMusicGenerator.js\"\nimport * as MusicGenerator from \"./MusicGenerator.js\"\n\nconst mp3Format: AudioFormat = {\n container: \"mp3\",\n encoding: \"mp3\",\n sampleRate: 44100,\n channels: 2,\n}\n\nconst result: MusicResult = {\n format: mp3Format,\n bytes: new Uint8Array([0xff, 0xfb, 0x90, 0x00]),\n durationSeconds: 30,\n lyrics: \"[Verse]\\nhello\\n\",\n watermark: { kind: \"synthid\" },\n}\n\nconst chunk = (n: number): AudioChunk => ({ bytes: new Uint8Array([n]) })\n\ndescribe(\"MusicGenerator.generate\", () => {\n it(\"returns the scripted MusicResult\", async () => {\n const mock = MockMusicGenerator.layer({ results: [result] })\n const program = MusicGenerator.generate({\n model: \"mock-music\",\n prompts: \"upbeat indie pop\",\n })\n const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(out.bytes).toEqual(result.bytes)\n expect(out.durationSeconds).toBe(30)\n expect(out.watermark?.kind).toBe(\"synthid\")\n expect(out.lyrics).toContain(\"[Verse]\")\n })\n\n it(\"records the request shape on the recorder\", async () => {\n const mock = MockMusicGenerator.layer({ results: [result, result] })\n const program = Effect.gen(function* () {\n yield* MusicGenerator.generate({ model: \"m\", prompts: \"techno\" })\n yield* MusicGenerator.generate({\n model: \"m\",\n prompts: [\n { text: \"synthwave\", weight: 1.0 },\n { text: \"80s movie OST\", weight: 0.4 },\n ],\n bpm: 120,\n instrumental: true,\n })\n return yield* mock.recorder\n })\n const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(rec.generateCalls.length).toBe(2)\n expect(rec.generateCalls[1]!.bpm).toBe(120)\n expect(rec.generateCalls[1]!.instrumental).toBe(true)\n expect(Array.isArray(rec.generateCalls[1]!.prompts)).toBe(true)\n })\n})\n\ndescribe(\"MusicGenerator.streamGeneration\", () => {\n it(\"emits scripted chunks\", async () => {\n const mock = MockMusicGenerator.layer({\n streamGenerationChunks: [[chunk(1), chunk(2), chunk(3)]],\n })\n const program = Stream.runCollect(\n MusicGenerator.streamGeneration({ model: \"m\", prompts: \"ambient\" }),\n )\n const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(out.map((c) => Array.from(c.bytes))).toEqual([[1], [2], [3]])\n })\n})\n\ndescribe(\"MusicGenerator capability marker (compile-time)\", () => {\n const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {\n model: \"m\",\n prompts: \"\",\n }\n\n it(\"requires `MusicInteractiveSession` on the R channel of streamGenerationFrom\", () => {\n const inputs = Stream.fromIterable([promptsInput([{ text: \"techno\" }])])\n const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))\n expectTypeOf(audio).toEqualTypeOf<\n Stream.Stream<\n AudioChunk,\n AiError.AiError,\n MusicGenerator.MusicGenerator | MusicGenerator.MusicInteractiveSession\n >\n >()\n })\n\n it(\"does NOT require `MusicInteractiveSession` for sync `generate`\", () => {\n const eff = MusicGenerator.generate({ model: \"m\", prompts: \"ambient\" })\n expectTypeOf(eff).toEqualTypeOf<\n Effect.Effect<MusicResult, AiError.AiError, MusicGenerator.MusicGenerator>\n >()\n })\n\n it(\"does NOT require `MusicInteractiveSession` for `streamGeneration`\", () => {\n const audio = MusicGenerator.streamGeneration({ model: \"m\", prompts: \"ambient\" })\n expectTypeOf(audio).toEqualTypeOf<\n Stream.Stream<AudioChunk, AiError.AiError, MusicGenerator.MusicGenerator>\n >()\n })\n\n it(\"a layer without the marker leaves `MusicInteractiveSession` unsatisfied in R\", () => {\n const noMarker = MockMusicGenerator.layerWithoutInteractive({})\n const inputs = Stream.fromIterable([promptsInput([{ text: \"techno\" }])])\n const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))\n const program = Stream.runDrain(audio).pipe(Effect.provide(noMarker.layer))\n expectTypeOf(program).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>\n >()\n })\n\n it(\"a full layer (with marker) clears R to never\", () => {\n const fullMock = MockMusicGenerator.layer({\n streamGenerationFromChunks: [[]],\n })\n const inputs = Stream.fromIterable([promptsInput([{ text: \"techno\" }])])\n const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))\n const program = Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))\n expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()\n })\n})\n\ndescribe(\"MusicGenerator.streamGenerationFrom\", () => {\n const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {\n model: \"lyria-realtime-001\",\n prompts: \"\",\n }\n\n it(\"drains a session-input stream and emits scripted audio\", async () => {\n const mock = MockMusicGenerator.layer({\n streamGenerationFromChunks: [[chunk(10), chunk(20)]],\n })\n const inputs = Stream.fromIterable([\n promptsInput([{ text: \"minimal techno\", weight: 1.0 }]),\n configInput({ bpm: 124 }),\n promptsInput([\n { text: \"minimal techno\", weight: 1.0 },\n { text: \"1980s synthwave\", weight: 0.3 },\n ]),\n ])\n const audio = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))\n const out = await Effect.runPromise(Stream.runCollect(audio).pipe(Effect.provide(mock.layer)))\n expect(out.map((c) => Array.from(c.bytes))).toEqual([[10], [20]])\n })\n\n it(\"records the request on the streamGenerationFrom call channel\", async () => {\n const mock = MockMusicGenerator.layer({\n streamGenerationFromChunks: [[chunk(42)]],\n })\n const program = Effect.gen(function* () {\n yield* Stream.runDrain(\n Stream.fromIterable([promptsInput([{ text: \"x\" }])]).pipe(\n MusicGenerator.streamGenerationFrom(sgfReq),\n ),\n )\n return yield* mock.recorder\n })\n const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(rec.streamGenerationFromCalls.length).toBe(1)\n expect(rec.streamGenerationFromCalls[0]!.model).toBe(\"lyria-realtime-001\")\n expect(rec.generateCalls.length).toBe(0)\n expect(rec.streamGenerationCalls.length).toBe(0)\n })\n})\n"],"mappings":"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"}
@@ -22,12 +22,12 @@ declare const withElapsed: <A, E, R>(self: Stream.Stream<A, E, R>) => Stream.Str
22
22
  * branch.
23
23
  */
24
24
  declare const timeToFirst: <A>(predicate: (a: A) => boolean) => <E, R>(self: Stream.Stream<A, E, R>) => Effect.Effect<Option.Option<Duration.Duration>, E, R>;
25
- interface RatePoint<A> {
25
+ type RatePoint<A> = {
26
26
  readonly value: A;
27
27
  readonly total: number;
28
28
  readonly ratePerSecond: number;
29
29
  readonly elapsed: Duration.Duration;
30
- }
30
+ };
31
31
  /**
32
32
  * Annotate every event with a running total and a rolling rate per second,
33
33
  * computed from a user-supplied weight function.
@@ -1 +1 @@
1
- {"version":3,"file":"Metrics.d.mts","names":[],"sources":["../../src/observability/Metrics.ts"],"mappings":";;;;;;;;;;;cAOa,WAAA,YACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MACzB,MAAA,CAAO,MAAA;EAAA,SAAkB,KAAA,EAAO,CAAA;EAAA,SAAY,OAAA,EAAS,QAAA,CAAS,QAAA;AAAA,GAAY,CAAA,EAAG,CAAA;AAFhF;;;;;;;;AAAA,cAwBa,WAAA,MACP,SAAA,GAAY,CAAA,EAAG,CAAA,wBACZ,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,QAAA,GAAW,CAAA,EAAG,CAAA;AAAA,UAO1E,SAAA;EAAA,SACN,KAAA,EAAO,CAAA;EAAA,SACP,KAAA;EAAA,SACA,aAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,QAAA;AAAA;;;;;;;;;;;;cAchB,QAAA,MACP,MAAA,GAAS,CAAA,EAAG,CAAA,uBACT,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,SAAA,CAAU,CAAA,GAAI,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"Metrics.d.mts","names":[],"sources":["../../src/observability/Metrics.ts"],"mappings":";;;;;;;;;;;cAOa,WAAA,YACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MACzB,MAAA,CAAO,MAAA;EAAA,SAAkB,KAAA,EAAO,CAAA;EAAA,SAAY,OAAA,EAAS,QAAA,CAAS,QAAA;AAAA,GAAY,CAAA,EAAG,CAAA;AAFhF;;;;;;;;AAAA,cAwBa,WAAA,MACP,SAAA,GAAY,CAAA,EAAG,CAAA,wBACZ,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,QAAA,GAAW,CAAA,EAAG,CAAA;AAAA,KAO/E,SAAA;EAAA,SACD,KAAA,EAAO,CAAA;EAAA,SACP,KAAA;EAAA,SACA,aAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,QAAA;AAAA;;;;;;;;;;;;cAchB,QAAA,MACP,MAAA,GAAS,CAAA,EAAG,CAAA,uBACT,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,SAAA,CAAU,CAAA,GAAI,CAAA,EAAG,CAAA"}
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
1
+ import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
2
  import { Clock, Duration, Effect, Option, Stream } from "effect";
3
3
  //#region src/observability/Metrics.ts
4
4
  var Metrics_exports = /* @__PURE__ */ __exportAll({
@@ -1 +1 @@
1
- {"version":3,"file":"Metrics.mjs","names":[],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import { Clock, Duration, Effect, Option, Stream } from \"effect\"\n\n/**\n * Annotate every event in a stream with the elapsed `Duration` since the\n * stream started consuming. The first event reports its time-from-start,\n * which is also the conventional \"time to first ____\" metric.\n */\nexport const withElapsed = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<{ readonly value: A; readonly elapsed: Duration.Duration }, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapEffect((value) =>\n Effect.map(Clock.currentTimeMillis, (now) => ({\n value,\n elapsed: Duration.millis(now - start),\n })),\n ),\n ),\n ),\n )\n\n/**\n * Compute the elapsed time until the first event matching the predicate.\n * Returns `Option.none()` if the stream completes without one.\n *\n * Consumes the stream. To track this *alongside* live consumption, use\n * `Stream.broadcast` to fan the source out and run `timeToFirst` on one\n * branch.\n */\nexport const timeToFirst =\n <A>(predicate: (a: A) => boolean) =>\n <E, R>(self: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<Duration.Duration>, E, R> =>\n withElapsed(self).pipe(\n Stream.filter(({ value }) => predicate(value)),\n Stream.runHead,\n Effect.map(Option.map(({ elapsed }) => elapsed)),\n )\n\nexport interface RatePoint<A> {\n readonly value: A\n readonly total: number\n readonly ratePerSecond: number\n readonly elapsed: Duration.Duration\n}\n\n/**\n * Annotate every event with a running total and a rolling rate per second,\n * computed from a user-supplied weight function.\n *\n * The weight is the unit you care about - bytes, tokens, error count, etc.\n * For tokens-per-second on `TurnEvent`, pass:\n *\n * `(d) => d.type === \"text_delta\" ? countTokens(d.text) : 0`\n *\n * Use any tokenizer you like; the library does not ship one.\n */\nexport const withRate =\n <A>(weight: (a: A) => number) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<RatePoint<A>, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapAccumEffect(\n () => ({ total: 0 }),\n (acc, value) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const total = acc.total + weight(value)\n const elapsedMs = now - start\n const ratePerSecond = elapsedMs > 0 ? (total / elapsedMs) * 1000 : 0\n return [\n { total },\n [\n {\n value,\n total,\n ratePerSecond,\n elapsed: Duration.millis(elapsedMs),\n } satisfies RatePoint<A>,\n ],\n ] as const\n }),\n ),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAa,eACX,SAEA,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,WAAW,UAChB,OAAO,IAAI,MAAM,oBAAoB,SAAS;CAC5C;CACA,SAAS,SAAS,OAAO,MAAM,MAAM;CACtC,EAAE,CACJ,CACF,CACF,CACF;;;;;;;;;AAUH,MAAa,eACP,eACG,SACL,YAAY,KAAK,CAAC,KAChB,OAAO,QAAQ,EAAE,YAAY,UAAU,MAAM,CAAC,EAC9C,OAAO,SACP,OAAO,IAAI,OAAO,KAAK,EAAE,cAAc,QAAQ,CAAC,CACjD;;;;;;;;;;;;AAoBL,MAAa,YACP,YACG,SACL,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,sBACE,EAAE,OAAO,GAAG,IAClB,KAAK,UACJ,OAAO,IAAI,MAAM,oBAAoB,QAAQ;CAC3C,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;CACvC,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,YAAY,IAAK,QAAQ,YAAa,MAAO;AACnE,QAAO,CACL,EAAE,OAAO,EACT,CACE;EACE;EACA;EACA;EACA,SAAS,SAAS,OAAO,UAAU;EACpC,CACF,CACF;EACD,CACL,CACF,CACF,CACF"}
1
+ {"version":3,"file":"Metrics.mjs","names":[],"sources":["../../src/observability/Metrics.ts"],"sourcesContent":["import { Clock, Duration, Effect, Option, Stream } from \"effect\"\n\n/**\n * Annotate every event in a stream with the elapsed `Duration` since the\n * stream started consuming. The first event reports its time-from-start,\n * which is also the conventional \"time to first ____\" metric.\n */\nexport const withElapsed = <A, E, R>(\n self: Stream.Stream<A, E, R>,\n): Stream.Stream<{ readonly value: A; readonly elapsed: Duration.Duration }, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapEffect((value) =>\n Effect.map(Clock.currentTimeMillis, (now) => ({\n value,\n elapsed: Duration.millis(now - start),\n })),\n ),\n ),\n ),\n )\n\n/**\n * Compute the elapsed time until the first event matching the predicate.\n * Returns `Option.none()` if the stream completes without one.\n *\n * Consumes the stream. To track this *alongside* live consumption, use\n * `Stream.broadcast` to fan the source out and run `timeToFirst` on one\n * branch.\n */\nexport const timeToFirst =\n <A>(predicate: (a: A) => boolean) =>\n <E, R>(self: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<Duration.Duration>, E, R> =>\n withElapsed(self).pipe(\n Stream.filter(({ value }) => predicate(value)),\n Stream.runHead,\n Effect.map(Option.map(({ elapsed }) => elapsed)),\n )\n\nexport type RatePoint<A> = {\n readonly value: A\n readonly total: number\n readonly ratePerSecond: number\n readonly elapsed: Duration.Duration\n}\n\n/**\n * Annotate every event with a running total and a rolling rate per second,\n * computed from a user-supplied weight function.\n *\n * The weight is the unit you care about - bytes, tokens, error count, etc.\n * For tokens-per-second on `TurnEvent`, pass:\n *\n * `(d) => d.type === \"text_delta\" ? countTokens(d.text) : 0`\n *\n * Use any tokenizer you like; the library does not ship one.\n */\nexport const withRate =\n <A>(weight: (a: A) => number) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<RatePoint<A>, E, R> =>\n Stream.unwrap(\n Effect.map(Clock.currentTimeMillis, (start) =>\n self.pipe(\n Stream.mapAccumEffect(\n () => ({ total: 0 }),\n (acc, value) =>\n Effect.map(Clock.currentTimeMillis, (now) => {\n const total = acc.total + weight(value)\n const elapsedMs = now - start\n const ratePerSecond = elapsedMs > 0 ? (total / elapsedMs) * 1000 : 0\n return [\n { total },\n [\n {\n value,\n total,\n ratePerSecond,\n elapsed: Duration.millis(elapsedMs),\n } satisfies RatePoint<A>,\n ],\n ] as const\n }),\n ),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAa,eACX,SAEA,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,WAAW,UAChB,OAAO,IAAI,MAAM,oBAAoB,SAAS;CAC5C;CACA,SAAS,SAAS,OAAO,MAAM,MAAM;CACtC,EAAE,CACJ,CACF,CACF,CACF;;;;;;;;;AAUH,MAAa,eACP,eACG,SACL,YAAY,KAAK,CAAC,KAChB,OAAO,QAAQ,EAAE,YAAY,UAAU,MAAM,CAAC,EAC9C,OAAO,SACP,OAAO,IAAI,OAAO,KAAK,EAAE,cAAc,QAAQ,CAAC,CACjD;;;;;;;;;;;;AAoBL,MAAa,YACP,YACG,SACL,OAAO,OACL,OAAO,IAAI,MAAM,oBAAoB,UACnC,KAAK,KACH,OAAO,sBACE,EAAE,OAAO,GAAG,IAClB,KAAK,UACJ,OAAO,IAAI,MAAM,oBAAoB,QAAQ;CAC3C,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;CACvC,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,YAAY,IAAK,QAAQ,YAAa,MAAO;AACnE,QAAO,CACL,EAAE,OAAO,EACT,CACE;EACE;EACA;EACA;EACA,SAAS,SAAS,OAAO,UAAU;EACpC,CACF,CACF;EACD,CACL,CACF,CACF,CACF"}
@@ -0,0 +1,96 @@
1
+ import { t as AiError } from "../AiError-csR8Bhxx.mjs";
2
+ import { n as AudioChunk, r as AudioFormat, t as AudioBlob } from "../Audio-BfCTGnH3.mjs";
3
+ import { Context, Effect, Stream } from "effect";
4
+
5
+ //#region src/speech-synthesizer/SpeechSynthesizer.d.ts
6
+ declare namespace SpeechSynthesizer_d_exports {
7
+ export { CommonStreamSynthesizeRequest, CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize };
8
+ }
9
+ /**
10
+ * Cross-provider synthesis request. Provider-specific extensions
11
+ * (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,
12
+ * MiniMax `vol` / `pitch`, Azure SSML style tags) live on each
13
+ * provider's typed request which extends this and narrows `model` and
14
+ * `voiceId`.
15
+ */
16
+ type CommonSynthesizeRequest = {
17
+ readonly text: string; /** Model identifier. Each provider narrows. */
18
+ readonly model: string;
19
+ /**
20
+ * Voice identifier. Per-provider request types narrow this to a
21
+ * typed literal union of stock voices + `(string & {})` escape for
22
+ * custom cloned voice IDs. Providers without custom-voice support
23
+ * (OpenAI, Deepgram Aura, AWS Polly) narrow to the stock-only union.
24
+ */
25
+ readonly voiceId: string;
26
+ readonly outputFormat?: AudioFormat;
27
+ readonly speed?: number;
28
+ readonly languageCode?: string;
29
+ };
30
+ /**
31
+ * Incremental-synthesis request — text arrives as `Stream<string>`.
32
+ * Gated by the `TtsIncrementalText` capability marker; only providers
33
+ * that ship the marker can be used.
34
+ *
35
+ * Multi-context features (Cartesia `context_id`, ElevenLabs `multi-
36
+ * stream-input`) are NOT exposed here — one logical utterance per
37
+ * call. Provider extensions can expose `forkContext` for that.
38
+ */
39
+ type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, "text">;
40
+ type SpeechSynthesizerService = {
41
+ /** One-shot. Full text in, full audio bytes out. Universally supported. */readonly synthesize: (request: CommonSynthesizeRequest) => Effect.Effect<AudioBlob, AiError>;
42
+ /**
43
+ * Full text in, audio chunks streamed out (chunked HTTP). Universally
44
+ * supported across providers that offer any streaming TTS at all.
45
+ */
46
+ readonly streamSynthesis: (request: CommonSynthesizeRequest) => Stream.Stream<AudioChunk, AiError>;
47
+ /**
48
+ * Incremental text in (as a Stream), audio chunks streamed out. The
49
+ * underlying WS connection is acquired on first pull and released
50
+ * when the output stream is finalized via `Stream.scoped`.
51
+ *
52
+ * Gated by the `TtsIncrementalText` capability marker on the top-
53
+ * level helper — providers without WS-style incremental input don't
54
+ * ship the marker, so calls fail at `Effect.provide` with a type
55
+ * error.
56
+ */
57
+ readonly streamSynthesisFrom: <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) => Stream.Stream<AudioChunk, AiError | E, R>;
58
+ };
59
+ declare const SpeechSynthesizer_base: Context.ServiceClass<SpeechSynthesizer, "@betalyra/effect-uai/SpeechSynthesizer", SpeechSynthesizerService>;
60
+ declare class SpeechSynthesizer extends SpeechSynthesizer_base {}
61
+ declare const TtsIncrementalText_base: Context.ServiceClass<TtsIncrementalText, "@betalyra/effect-uai/capability/TtsIncrementalText", void>;
62
+ /**
63
+ * Capability marker — provided by provider layers whose
64
+ * `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure
65
+ * (wire), and AWS Polly non-Generative do not ship it. Calling
66
+ * `streamSynthesisFrom` while only one of those Layers is in scope
67
+ * fails at `Effect.provide` with a type error.
68
+ *
69
+ * Phantom — the value is `void`; providers register with
70
+ * `Layer.succeed(TtsIncrementalText, undefined)`.
71
+ */
72
+ declare class TtsIncrementalText extends TtsIncrementalText_base {}
73
+ /** One-shot synthesis. */
74
+ declare const synthesize: (request: CommonSynthesizeRequest) => Effect.Effect<AudioBlob, AiError, SpeechSynthesizer>;
75
+ /** Full text in, audio chunks out. */
76
+ declare const streamSynthesis: (request: CommonSynthesizeRequest) => Stream.Stream<AudioChunk, AiError, SpeechSynthesizer>;
77
+ /**
78
+ * Incremental synthesis. Dual-arity: pipeable (data-last) and direct
79
+ * (data-first). Requires `TtsIncrementalText` in R — providers without
80
+ * incremental-text-in support are a type error at provide time.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const audio = LanguageModel.streamTurn(turnReq).pipe(
85
+ * Stream.filterMap(Turn.toTextDelta),
86
+ * SpeechSynthesizer.streamSynthesisFrom(synthReq),
87
+ * )
88
+ * ```
89
+ */
90
+ declare const streamSynthesisFrom: {
91
+ (request: CommonStreamSynthesizeRequest): <E, R>(textIn: Stream.Stream<string, E, R>) => Stream.Stream<AudioChunk, AiError | E, R | SpeechSynthesizer | TtsIncrementalText>;
92
+ <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest): Stream.Stream<AudioChunk, AiError | E, R | SpeechSynthesizer | TtsIncrementalText>;
93
+ };
94
+ //#endregion
95
+ export { CommonStreamSynthesizeRequest, CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize, SpeechSynthesizer_d_exports as t };
96
+ //# sourceMappingURL=SpeechSynthesizer.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpeechSynthesizer.d.mts","names":[],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;KAWY,uBAAA;EAAA,SACD,IAAA;WAEA,KAAA;;;AAHX;;;;WAUW,OAAA;EAAA,SACA,YAAA,GAAe,WAAA;EAAA,SACf,KAAA;EAAA,SACA,YAAA;AAAA;;;;;AAYX;;;;;KAAY,6BAAA,GAAgC,IAAA,CAAK,uBAAA;AAAA,KAErC,wBAAA;EAAwB,oFAEzB,UAAA,GACP,OAAA,EAAS,uBAAA,KACN,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA;EAAX;;;;EAAA,SAKV,eAAA,GACP,OAAA,EAAS,uBAAA,KACN,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA;EAAA;;;;;;;;;;EAAA,SAWtB,mBAAA,SACP,MAAA,EAAQ,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,GACjC,OAAA,EAAS,6BAAA,KACN,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,GAAkB,CAAA,EAAG,CAAA;AAAA;AAAA,cACrD,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,cAGS,uBAAA;;;;;;;;;;;cAYnC,kBAAA,SAA2B,uBAAA;;cAK3B,UAAA,GACX,OAAA,EAAS,uBAAA,KACR,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,EAAiB,iBAAA;;cAIhC,eAAA,GACX,OAAA,EAAS,uBAAA,KACR,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,EAAiB,iBAAA;;;;;;;;;;;;;;cAgBjC,mBAAA;EAAA,CAET,OAAA,EAAS,6BAAA,UAET,MAAA,EAAQ,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAC9B,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,GAAkB,CAAA,EAAG,CAAA,GAAI,iBAAA,GAAoB,kBAAA;EAAA,OAE1E,MAAA,EAAQ,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,GACjC,OAAA,EAAS,6BAAA,GACR,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,GAAkB,CAAA,EAAG,CAAA,GAAI,iBAAA,GAAoB,kBAAA;AAAA"}
@@ -0,0 +1,48 @@
1
+ import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
+ import { Context, Effect, Function, Stream } from "effect";
3
+ //#region src/speech-synthesizer/SpeechSynthesizer.ts
4
+ var SpeechSynthesizer_exports = /* @__PURE__ */ __exportAll({
5
+ SpeechSynthesizer: () => SpeechSynthesizer,
6
+ TtsIncrementalText: () => TtsIncrementalText,
7
+ streamSynthesis: () => streamSynthesis,
8
+ streamSynthesisFrom: () => streamSynthesisFrom,
9
+ synthesize: () => synthesize
10
+ });
11
+ var SpeechSynthesizer = class extends Context.Service()("@betalyra/effect-uai/SpeechSynthesizer") {};
12
+ /**
13
+ * Capability marker — provided by provider layers whose
14
+ * `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure
15
+ * (wire), and AWS Polly non-Generative do not ship it. Calling
16
+ * `streamSynthesisFrom` while only one of those Layers is in scope
17
+ * fails at `Effect.provide` with a type error.
18
+ *
19
+ * Phantom — the value is `void`; providers register with
20
+ * `Layer.succeed(TtsIncrementalText, undefined)`.
21
+ */
22
+ var TtsIncrementalText = class extends Context.Service()("@betalyra/effect-uai/capability/TtsIncrementalText") {};
23
+ /** One-shot synthesis. */
24
+ const synthesize = (request) => Effect.flatMap(SpeechSynthesizer.asEffect(), (s) => s.synthesize(request));
25
+ /** Full text in, audio chunks out. */
26
+ const streamSynthesis = (request) => Stream.unwrap(Effect.map(SpeechSynthesizer.asEffect(), (s) => s.streamSynthesis(request)));
27
+ /**
28
+ * Incremental synthesis. Dual-arity: pipeable (data-last) and direct
29
+ * (data-first). Requires `TtsIncrementalText` in R — providers without
30
+ * incremental-text-in support are a type error at provide time.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const audio = LanguageModel.streamTurn(turnReq).pipe(
35
+ * Stream.filterMap(Turn.toTextDelta),
36
+ * SpeechSynthesizer.streamSynthesisFrom(synthReq),
37
+ * )
38
+ * ```
39
+ */
40
+ const streamSynthesisFrom = Function.dual(2, (textIn, request) => Stream.unwrap(Effect.gen(function* () {
41
+ const s = yield* SpeechSynthesizer.asEffect();
42
+ yield* TtsIncrementalText.asEffect();
43
+ return s.streamSynthesisFrom(textIn, request);
44
+ })));
45
+ //#endregion
46
+ export { SpeechSynthesizer, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize, SpeechSynthesizer_exports as t };
47
+
48
+ //# sourceMappingURL=SpeechSynthesizer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpeechSynthesizer.mjs","names":[],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Function, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"../domain/Audio.js\"\n\n/**\n * Cross-provider synthesis request. Provider-specific extensions\n * (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,\n * MiniMax `vol` / `pitch`, Azure SSML style tags) live on each\n * provider's typed request which extends this and narrows `model` and\n * `voiceId`.\n */\nexport type CommonSynthesizeRequest = {\n readonly text: string\n /** Model identifier. Each provider narrows. */\n readonly model: string\n /**\n * Voice identifier. Per-provider request types narrow this to a\n * typed literal union of stock voices + `(string & {})` escape for\n * custom cloned voice IDs. Providers without custom-voice support\n * (OpenAI, Deepgram Aura, AWS Polly) narrow to the stock-only union.\n */\n readonly voiceId: string\n readonly outputFormat?: AudioFormat\n readonly speed?: number\n readonly languageCode?: string\n}\n\n/**\n * Incremental-synthesis request — text arrives as `Stream<string>`.\n * Gated by the `TtsIncrementalText` capability marker; only providers\n * that ship the marker can be used.\n *\n * Multi-context features (Cartesia `context_id`, ElevenLabs `multi-\n * stream-input`) are NOT exposed here — one logical utterance per\n * call. Provider extensions can expose `forkContext` for that.\n */\nexport type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, \"text\">\n\nexport type SpeechSynthesizerService = {\n /** One-shot. Full text in, full audio bytes out. Universally supported. */\n readonly synthesize: (\n request: CommonSynthesizeRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Full text in, audio chunks streamed out (chunked HTTP). Universally\n * supported across providers that offer any streaming TTS at all.\n */\n readonly streamSynthesis: (\n request: CommonSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n /**\n * Incremental text in (as a Stream), audio chunks streamed out. The\n * underlying WS connection is acquired on first pull and released\n * when the output stream is finalized via `Stream.scoped`.\n *\n * Gated by the `TtsIncrementalText` capability marker on the top-\n * level helper — providers without WS-style incremental input don't\n * ship the marker, so calls fail at `Effect.provide` with a type\n * error.\n */\n readonly streamSynthesisFrom: <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>\n}\n\nexport class SpeechSynthesizer extends Context.Service<\n SpeechSynthesizer,\n SpeechSynthesizerService\n>()(\"@betalyra/effect-uai/SpeechSynthesizer\") {}\n\n/**\n * Capability marker — provided by provider layers whose\n * `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure\n * (wire), and AWS Polly non-Generative do not ship it. Calling\n * `streamSynthesisFrom` while only one of those Layers is in scope\n * fails at `Effect.provide` with a type error.\n *\n * Phantom — the value is `void`; providers register with\n * `Layer.succeed(TtsIncrementalText, undefined)`.\n */\nexport class TtsIncrementalText extends Context.Service<TtsIncrementalText, void>()(\n \"@betalyra/effect-uai/capability/TtsIncrementalText\",\n) {}\n\n/** One-shot synthesis. */\nexport const synthesize = (\n request: CommonSynthesizeRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer> =>\n Effect.flatMap(SpeechSynthesizer.asEffect(), (s) => s.synthesize(request))\n\n/** Full text in, audio chunks out. */\nexport const streamSynthesis = (\n request: CommonSynthesizeRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer> =>\n Stream.unwrap(Effect.map(SpeechSynthesizer.asEffect(), (s) => s.streamSynthesis(request)))\n\n/**\n * Incremental synthesis. Dual-arity: pipeable (data-last) and direct\n * (data-first). Requires `TtsIncrementalText` in R — providers without\n * incremental-text-in support are a type error at provide time.\n *\n * @example\n * ```ts\n * const audio = LanguageModel.streamTurn(turnReq).pipe(\n * Stream.filterMap(Turn.toTextDelta),\n * SpeechSynthesizer.streamSynthesisFrom(synthReq),\n * )\n * ```\n */\nexport const streamSynthesisFrom: {\n (\n request: CommonStreamSynthesizeRequest,\n ): <E, R>(\n textIn: Stream.Stream<string, E, R>,\n ) => Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: CommonStreamSynthesizeRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>\n} = Function.dual(\n 2,\n <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* TtsIncrementalText.asEffect()\n return s.streamSynthesisFrom(textIn, request)\n }),\n ),\n)\n"],"mappings":";;;;;;;;;;AAkEA,IAAa,oBAAb,cAAuC,QAAQ,SAG5C,CAAC,yCAAyC,CAAC;;;;;;;;;;;AAY9C,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;AAGF,MAAa,cACX,YAEA,OAAO,QAAQ,kBAAkB,UAAU,GAAG,MAAM,EAAE,WAAW,QAAQ,CAAC;;AAG5E,MAAa,mBACX,YAEA,OAAO,OAAO,OAAO,IAAI,kBAAkB,UAAU,GAAG,MAAM,EAAE,gBAAgB,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;AAe5F,MAAa,sBAUT,SAAS,KACX,IACO,QAAqC,YAC1C,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,mBAAmB,UAAU;AACpC,QAAO,EAAE,oBAAoB,QAAQ,QAAQ;EAC7C,CACH,CACJ"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,112 @@
1
+ import { streamSynthesis, streamSynthesisFrom, synthesize } from "./SpeechSynthesizer.mjs";
2
+ import { i as it, n as globalExpect, r as describe, t as import_dist } from "../dist-DV5ISja1.mjs";
3
+ import { layer, layerWithoutIncremental } from "../testing/MockSpeechSynthesizer.mjs";
4
+ import { Effect, Stream } from "effect";
5
+ //#region src/speech-synthesizer/SpeechSynthesizer.test.ts
6
+ const blob = {
7
+ format: {
8
+ container: "raw",
9
+ encoding: "pcm_s16le",
10
+ sampleRate: 24e3
11
+ },
12
+ bytes: new Uint8Array([
13
+ 222,
14
+ 173,
15
+ 190,
16
+ 239
17
+ ]),
18
+ durationSeconds: .5
19
+ };
20
+ const chunk = (n) => ({ bytes: new Uint8Array([n]) });
21
+ describe("SpeechSynthesizer.synthesize", () => {
22
+ it("returns the scripted AudioBlob", async () => {
23
+ const mock = layer({ blobs: [blob] });
24
+ const program = synthesize({
25
+ text: "hi",
26
+ model: "mock-tts",
27
+ voiceId: "stock-voice"
28
+ });
29
+ const result = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
30
+ globalExpect(result.bytes).toEqual(blob.bytes);
31
+ globalExpect(result.durationSeconds).toBe(.5);
32
+ });
33
+ });
34
+ describe("SpeechSynthesizer.streamSynthesis", () => {
35
+ it("emits scripted chunks for full-text-in streaming", async () => {
36
+ const mock = layer({ streamSynthesisChunks: [[
37
+ chunk(1),
38
+ chunk(2),
39
+ chunk(3)
40
+ ]] });
41
+ const program = Stream.runCollect(streamSynthesis({
42
+ text: "hi",
43
+ model: "mock-tts",
44
+ voiceId: "stock-voice"
45
+ }));
46
+ globalExpect((await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))).map((c) => Array.from(c.bytes))).toEqual([
47
+ [1],
48
+ [2],
49
+ [3]
50
+ ]);
51
+ });
52
+ });
53
+ describe("SpeechSynthesizer capability marker (compile-time)", () => {
54
+ const ssfReq = {
55
+ model: "mock-tts",
56
+ voiceId: "v"
57
+ };
58
+ it("requires `TtsIncrementalText` on the R channel of streamSynthesisFrom", () => {
59
+ (0, import_dist.expectTypeOf)(Stream.fromIterable(["a"]).pipe(streamSynthesisFrom(ssfReq))).toEqualTypeOf();
60
+ });
61
+ it("does NOT require `TtsIncrementalText` for sync `synthesize`", () => {
62
+ (0, import_dist.expectTypeOf)(synthesize({
63
+ text: "hi",
64
+ model: "m",
65
+ voiceId: "v"
66
+ })).toEqualTypeOf();
67
+ });
68
+ it("does NOT require `TtsIncrementalText` for full-text `streamSynthesis`", () => {
69
+ (0, import_dist.expectTypeOf)(streamSynthesis({
70
+ text: "hi",
71
+ model: "m",
72
+ voiceId: "v"
73
+ })).toEqualTypeOf();
74
+ });
75
+ it("a layer without the marker leaves `TtsIncrementalText` unsatisfied in R", () => {
76
+ const noMarker = layerWithoutIncremental({});
77
+ const audio = Stream.fromIterable(["a"]).pipe(streamSynthesisFrom(ssfReq));
78
+ (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(noMarker.layer))).toEqualTypeOf();
79
+ });
80
+ it("a full layer (with marker) clears R to never", () => {
81
+ const fullMock = layer({ streamSynthesisFromChunks: [[]] });
82
+ const audio = Stream.fromIterable(["a"]).pipe(streamSynthesisFrom(ssfReq));
83
+ (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))).toEqualTypeOf();
84
+ });
85
+ });
86
+ describe("SpeechSynthesizer.streamSynthesisFrom", () => {
87
+ const ssfReq = {
88
+ model: "mock-tts",
89
+ voiceId: "stock-voice"
90
+ };
91
+ it("pipes an LLM-style text stream into audio chunks", async () => {
92
+ const mock = layer({ streamSynthesisFromChunks: [[chunk(10), chunk(20)]] });
93
+ const audio = Stream.fromIterable(["Hello, ", "world."]).pipe(streamSynthesisFrom(ssfReq));
94
+ globalExpect((await Effect.runPromise(Stream.runCollect(audio).pipe(Effect.provide(mock.layer)))).map((c) => Array.from(c.bytes))).toEqual([[10], [20]]);
95
+ });
96
+ it("records the request on the streamSynthesisFrom call channel", async () => {
97
+ const mock = layer({ streamSynthesisFromChunks: [[chunk(42)]] });
98
+ const program = Effect.gen(function* () {
99
+ yield* Stream.runDrain(Stream.fromIterable(["x"]).pipe(streamSynthesisFrom(ssfReq)));
100
+ return yield* mock.recorder;
101
+ });
102
+ const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
103
+ globalExpect(rec.streamSynthesisFromCalls.length).toBe(1);
104
+ globalExpect(rec.streamSynthesisFromCalls[0].voiceId).toBe("stock-voice");
105
+ globalExpect(rec.synthesizeCalls.length).toBe(0);
106
+ globalExpect(rec.streamSynthesisCalls.length).toBe(0);
107
+ });
108
+ });
109
+ //#endregion
110
+ export {};
111
+
112
+ //# sourceMappingURL=SpeechSynthesizer.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpeechSynthesizer.test.mjs","names":["MockSpeechSynthesizer.layer","SpeechSynthesizer.synthesize","SpeechSynthesizer.streamSynthesis","SpeechSynthesizer.streamSynthesisFrom","MockSpeechSynthesizer.layerWithoutIncremental"],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.test.ts"],"sourcesContent":["import { Effect, Stream } from \"effect\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"../domain/AiError.js\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"../domain/Audio.js\"\nimport * as MockSpeechSynthesizer from \"../testing/MockSpeechSynthesizer.js\"\nimport * as SpeechSynthesizer from \"./SpeechSynthesizer.js\"\n\nconst pcmFormat: AudioFormat = {\n container: \"raw\",\n encoding: \"pcm_s16le\",\n sampleRate: 24000,\n}\n\nconst blob: AudioBlob = {\n format: pcmFormat,\n bytes: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),\n durationSeconds: 0.5,\n}\n\nconst chunk = (n: number): AudioChunk => ({ bytes: new Uint8Array([n]) })\n\ndescribe(\"SpeechSynthesizer.synthesize\", () => {\n it(\"returns the scripted AudioBlob\", async () => {\n const mock = MockSpeechSynthesizer.layer({ blobs: [blob] })\n const program = SpeechSynthesizer.synthesize({\n text: \"hi\",\n model: \"mock-tts\",\n voiceId: \"stock-voice\",\n })\n const result = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(result.bytes).toEqual(blob.bytes)\n expect(result.durationSeconds).toBe(0.5)\n })\n})\n\ndescribe(\"SpeechSynthesizer.streamSynthesis\", () => {\n it(\"emits scripted chunks for full-text-in streaming\", async () => {\n const mock = MockSpeechSynthesizer.layer({\n streamSynthesisChunks: [[chunk(1), chunk(2), chunk(3)]],\n })\n const program = Stream.runCollect(\n SpeechSynthesizer.streamSynthesis({\n text: \"hi\",\n model: \"mock-tts\",\n voiceId: \"stock-voice\",\n }),\n )\n const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(out.map((c) => Array.from(c.bytes))).toEqual([[1], [2], [3]])\n })\n})\n\ndescribe(\"SpeechSynthesizer capability marker (compile-time)\", () => {\n const ssfReq: SpeechSynthesizer.CommonStreamSynthesizeRequest = {\n model: \"mock-tts\",\n voiceId: \"v\",\n }\n\n it(\"requires `TtsIncrementalText` on the R channel of streamSynthesisFrom\", () => {\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"a\"])\n const audio = tokens.pipe(SpeechSynthesizer.streamSynthesisFrom(ssfReq))\n expectTypeOf(audio).toEqualTypeOf<\n Stream.Stream<\n AudioChunk,\n AiError.AiError,\n SpeechSynthesizer.SpeechSynthesizer | SpeechSynthesizer.TtsIncrementalText\n >\n >()\n })\n\n it(\"does NOT require `TtsIncrementalText` for sync `synthesize`\", () => {\n const eff = SpeechSynthesizer.synthesize({ text: \"hi\", model: \"m\", voiceId: \"v\" })\n expectTypeOf(eff).toEqualTypeOf<\n Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer.SpeechSynthesizer>\n >()\n })\n\n it(\"does NOT require `TtsIncrementalText` for full-text `streamSynthesis`\", () => {\n const audio = SpeechSynthesizer.streamSynthesis({ text: \"hi\", model: \"m\", voiceId: \"v\" })\n expectTypeOf(audio).toEqualTypeOf<\n Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer.SpeechSynthesizer>\n >()\n })\n\n it(\"a layer without the marker leaves `TtsIncrementalText` unsatisfied in R\", () => {\n const noMarker = MockSpeechSynthesizer.layerWithoutIncremental({})\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"a\"])\n const audio = tokens.pipe(SpeechSynthesizer.streamSynthesisFrom(ssfReq))\n const program = Stream.runDrain(audio).pipe(Effect.provide(noMarker.layer))\n // `SpeechSynthesizer` is provided by the layer; `TtsIncrementalText` is not,\n // so it remains in R — calling `Effect.runPromise(program)` would be a type\n // error because runPromise requires `R = never`.\n expectTypeOf(program).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, SpeechSynthesizer.TtsIncrementalText>\n >()\n })\n\n it(\"a full layer (with marker) clears R to never\", () => {\n const fullMock = MockSpeechSynthesizer.layer({\n streamSynthesisFromChunks: [[]],\n })\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"a\"])\n const audio = tokens.pipe(SpeechSynthesizer.streamSynthesisFrom(ssfReq))\n const program = Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))\n expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()\n })\n})\n\ndescribe(\"SpeechSynthesizer.streamSynthesisFrom\", () => {\n const ssfReq: SpeechSynthesizer.CommonStreamSynthesizeRequest = {\n model: \"mock-tts\",\n voiceId: \"stock-voice\",\n }\n\n it(\"pipes an LLM-style text stream into audio chunks\", async () => {\n const mock = MockSpeechSynthesizer.layer({\n streamSynthesisFromChunks: [[chunk(10), chunk(20)]],\n })\n const tokens = Stream.fromIterable([\"Hello, \", \"world.\"])\n const audio = tokens.pipe(SpeechSynthesizer.streamSynthesisFrom(ssfReq))\n const out = await Effect.runPromise(Stream.runCollect(audio).pipe(Effect.provide(mock.layer)))\n expect(out.map((c) => Array.from(c.bytes))).toEqual([[10], [20]])\n })\n\n it(\"records the request on the streamSynthesisFrom call channel\", async () => {\n const mock = MockSpeechSynthesizer.layer({\n streamSynthesisFromChunks: [[chunk(42)]],\n })\n const program = Effect.gen(function* () {\n yield* Stream.runDrain(\n Stream.fromIterable([\"x\"]).pipe(SpeechSynthesizer.streamSynthesisFrom(ssfReq)),\n )\n return yield* mock.recorder\n })\n const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(rec.streamSynthesisFromCalls.length).toBe(1)\n expect(rec.streamSynthesisFromCalls[0]!.voiceId).toBe(\"stock-voice\")\n expect(rec.synthesizeCalls.length).toBe(0)\n expect(rec.streamSynthesisCalls.length).toBe(0)\n })\n})\n"],"mappings":";;;;;AAaA,MAAM,OAAkB;CACtB,QAAQ;EANR,WAAW;EACX,UAAU;EACV,YAAY;EAIK;CACjB,OAAO,IAAI,WAAW;EAAC;EAAM;EAAM;EAAM;EAAK,CAAC;CAC/C,iBAAiB;CAClB;AAED,MAAM,SAAS,OAA2B,EAAE,OAAO,IAAI,WAAW,CAAC,EAAE,CAAC,EAAE;AAExE,SAAS,sCAAsC;AAC7C,IAAG,kCAAkC,YAAY;EAC/C,MAAM,OAAOA,MAA4B,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;EAC3D,MAAM,UAAUC,WAA6B;GAC3C,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC;EACF,MAAM,SAAS,MAAM,OAAO,WAAW,QAAQ,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC;AAChF,eAAO,OAAO,MAAM,CAAC,QAAQ,KAAK,MAAM;AACxC,eAAO,OAAO,gBAAgB,CAAC,KAAK,GAAI;GACxC;EACF;AAEF,SAAS,2CAA2C;AAClD,IAAG,oDAAoD,YAAY;EACjE,MAAM,OAAOD,MAA4B,EACvC,uBAAuB,CAAC;GAAC,MAAM,EAAE;GAAE,MAAM,EAAE;GAAE,MAAM,EAAE;GAAC,CAAC,EACxD,CAAC;EACF,MAAM,UAAU,OAAO,WACrBE,gBAAkC;GAChC,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC,CACH;AAED,gBAAO,MADW,OAAO,WAAW,QAAQ,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,EAClE,KAAK,MAAM,MAAM,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ;GAAC,CAAC,EAAE;GAAE,CAAC,EAAE;GAAE,CAAC,EAAE;GAAC,CAAC;GACpE;EACF;AAEF,SAAS,4DAA4D;CACnE,MAAM,SAA0D;EAC9D,OAAO;EACP,SAAS;EACV;AAED,IAAG,+EAA+E;AAGhF,GAAA,GAAA,YAAA,cAFsC,OAAO,aAAa,CAAC,IAAI,CAC3C,CAAC,KAAKC,oBAAsC,OAAO,CACrD,CAAC,CAAC,eAMjB;GACH;AAEF,IAAG,qEAAqE;AAEtE,GAAA,GAAA,YAAA,cADYF,WAA6B;GAAE,MAAM;GAAM,OAAO;GAAK,SAAS;GAAK,CACjE,CAAC,CAAC,eAEf;GACH;AAEF,IAAG,+EAA+E;AAEhF,GAAA,GAAA,YAAA,cADcC,gBAAkC;GAAE,MAAM;GAAM,OAAO;GAAK,SAAS;GAAK,CACtE,CAAC,CAAC,eAEjB;GACH;AAEF,IAAG,iFAAiF;EAClF,MAAM,WAAWE,wBAA8C,EAAE,CAAC;EAElE,MAAM,QADgC,OAAO,aAAa,CAAC,IAAI,CAC3C,CAAC,KAAKD,oBAAsC,OAAO,CAAC;AAKxE,GAAA,GAAA,YAAA,cAJgB,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO,QAAQ,SAAS,MAAM,CAItD,CAAC,CAAC,eAEnB;GACH;AAEF,IAAG,sDAAsD;EACvD,MAAM,WAAWH,MAA4B,EAC3C,2BAA2B,CAAC,EAAE,CAAC,EAChC,CAAC;EAEF,MAAM,QADgC,OAAO,aAAa,CAAC,IAAI,CAC3C,CAAC,KAAKG,oBAAsC,OAAO,CAAC;AAExE,GAAA,GAAA,YAAA,cADgB,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO,QAAQ,SAAS,MAAM,CACtD,CAAC,CAAC,eAA4D;GAClF;EACF;AAEF,SAAS,+CAA+C;CACtD,MAAM,SAA0D;EAC9D,OAAO;EACP,SAAS;EACV;AAED,IAAG,oDAAoD,YAAY;EACjE,MAAM,OAAOH,MAA4B,EACvC,2BAA2B,CAAC,CAAC,MAAM,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EACpD,CAAC;EAEF,MAAM,QADS,OAAO,aAAa,CAAC,WAAW,SAAS,CACpC,CAAC,KAAKG,oBAAsC,OAAO,CAAC;AAExE,gBAAO,MADW,OAAO,WAAW,OAAO,WAAW,MAAM,CAAC,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,EACnF,KAAK,MAAM,MAAM,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;GACjE;AAEF,IAAG,+DAA+D,YAAY;EAC5E,MAAM,OAAOH,MAA4B,EACvC,2BAA2B,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EACzC,CAAC;EACF,MAAM,UAAU,OAAO,IAAI,aAAa;AACtC,UAAO,OAAO,SACZ,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,KAAKG,oBAAsC,OAAO,CAAC,CAC/E;AACD,UAAO,OAAO,KAAK;IACnB;EACF,MAAM,MAAM,MAAM,OAAO,WAAW,QAAQ,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC7E,eAAO,IAAI,yBAAyB,OAAO,CAAC,KAAK,EAAE;AACnD,eAAO,IAAI,yBAAyB,GAAI,QAAQ,CAAC,KAAK,cAAc;AACpE,eAAO,IAAI,gBAAgB,OAAO,CAAC,KAAK,EAAE;AAC1C,eAAO,IAAI,qBAAqB,OAAO,CAAC,KAAK,EAAE;GAC/C;EACF"}
@@ -1,10 +1,10 @@
1
- import { Schema, Stream } from "effect";
1
+ import { Effect, Schema, Stream } from "effect";
2
2
  import * as _$effect_Types0 from "effect/Types";
3
3
  import * as _$effect_Cause0 from "effect/Cause";
4
4
 
5
5
  //#region src/streaming/JSONL.d.ts
6
6
  declare namespace JSONL_d_exports {
7
- export { JsonParseError, fromBytes, parse, toBytes };
7
+ export { JsonParseError, fromBytes, parse, parseSafe, toBytes };
8
8
  }
9
9
  declare const JsonParseError_base: new <A extends Record<string, any> = {}>(args: _$effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => _$effect_Cause0.YieldableError & {
10
10
  readonly _tag: "JsonParseError";
@@ -24,11 +24,18 @@ declare const fromBytes: <E, R>(self: Stream.Stream<Uint8Array, E, R>) => Stream
24
24
  * uniformly.
25
25
  */
26
26
  declare const parse: <A, I>(schema: Schema.Codec<A, I>) => <E, R>(self: Stream.Stream<string, E, R>) => Stream.Stream<A, JsonParseError | E, R>;
27
+ /**
28
+ * Best-effort parse of a single JSON frame. Returns the parsed value or
29
+ * `undefined` on malformed input. Realtime WS adapters use this to skip
30
+ * non-JSON or partially-received frames silently rather than fail the
31
+ * entire session over one bad frame.
32
+ */
33
+ declare const parseSafe: (raw: string) => Effect.Effect<unknown, never, never>;
27
34
  /**
28
35
  * Serialize a stream of values to JSONL bytes. Encodes each value via
29
36
  * `Schema.encodeUnknownSync`. Each line ends with `\n`.
30
37
  */
31
38
  declare const toBytes: <A, I>(schema: Schema.Codec<A, I>) => <E, R>(self: Stream.Stream<A, E, R>) => Stream.Stream<Uint8Array, E, R>;
32
39
  //#endregion
33
- export { JsonParseError, fromBytes, parse, JSONL_d_exports as t, toBytes };
40
+ export { JsonParseError, fromBytes, parse, parseSafe, JSONL_d_exports as t, toBytes };
34
41
  //# sourceMappingURL=JSONL.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;cAAqD,mBAAA;;;cAExC,cAAA,SAAuB,mBAAA;EAAA,SACzB,IAAA;EAAA,SACA,KAAA;AAAA;;;;AAJ0C;cAgDxC,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;;;;;;cAaf,KAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,cAAA,GAAiB,CAAA,EAAG,CAAA;;;;;cAsBrE,OAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"JSONL.d.mts","names":[],"sources":["../../src/streaming/JSONL.ts"],"mappings":";;;;;;;;cAAqD,mBAAA;;;cAExC,cAAA,SAAuB,mBAAA;EAAA,SACzB,IAAA;EAAA,SACA,KAAA;AAAA;;;;;cA4CE,SAAA,SACX,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,MAClC,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA;;;;;;cAaf,KAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,cAAA,GAAiB,CAAA,EAAG,CAAA;;;;;;;cAsBrE,SAAA,GAAa,GAAA,aAAW,MAAA,CAAA,MAAA;;;;;cAYxB,OAAA,SACJ,MAAA,EAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,CAAA,aACxB,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA"}
@@ -1,10 +1,11 @@
1
- import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
1
+ import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
2
  import { Data, Effect, Schema, Stream } from "effect";
3
3
  //#region src/streaming/JSONL.ts
4
4
  var JSONL_exports = /* @__PURE__ */ __exportAll({
5
5
  JsonParseError: () => JsonParseError,
6
6
  fromBytes: () => fromBytes,
7
7
  parse: () => parse,
8
+ parseSafe: () => parseSafe,
8
9
  toBytes: () => toBytes
9
10
  });
10
11
  var JsonParseError = class extends Data.TaggedError("JsonParseError") {};
@@ -36,6 +37,16 @@ const parse = (schema) => (self) => self.pipe(Stream.mapEffect((line) => Effect.
36
37
  line,
37
38
  cause
38
39
  })))))));
40
+ /**
41
+ * Best-effort parse of a single JSON frame. Returns the parsed value or
42
+ * `undefined` on malformed input. Realtime WS adapters use this to skip
43
+ * non-JSON or partially-received frames silently rather than fail the
44
+ * entire session over one bad frame.
45
+ */
46
+ const parseSafe = (raw) => Effect.try({
47
+ try: () => JSON.parse(raw),
48
+ catch: () => void 0
49
+ }).pipe(Effect.orElseSucceed(() => void 0));
39
50
  const encoder = new TextEncoder();
40
51
  /**
41
52
  * Serialize a stream of values to JSONL bytes. Encodes each value via
@@ -46,6 +57,6 @@ const toBytes = (schema) => (self) => self.pipe(Stream.map((value) => {
46
57
  return encoder.encode(JSON.stringify(encoded) + "\n");
47
58
  }));
48
59
  //#endregion
49
- export { JsonParseError, fromBytes, parse, JSONL_exports as t, toBytes };
60
+ export { JsonParseError, fromBytes, parse, parseSafe, JSONL_exports as t, toBytes };
50
61
 
51
62
  //# sourceMappingURL=JSONL.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local; see SSE.ts for the same shape).\n// ---------------------------------------------------------------------------\n\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited\n * lines. Empty lines are skipped. Buffers across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")),\n splitOn(\"\\n\"),\n Stream.filter((line) => line.length > 0),\n )\n\n/**\n * Validate each JSONL line against a Schema. JSON parse errors and Schema\n * decode errors both surface as a `JsonParseError` so callers can `catchTag`\n * uniformly.\n */\nexport const parse =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<A, JsonParseError | E, R> =>\n self.pipe(\n Stream.mapEffect((line) =>\n Effect.try({\n try: () => JSON.parse(line) as unknown,\n catch: (cause) => new JsonParseError({ line, cause }),\n }).pipe(\n Effect.flatMap((value) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError((cause) => new JsonParseError({ line, cause })),\n ),\n ),\n ),\n ),\n )\n\nconst encoder = new TextEncoder()\n\n/**\n * Serialize a stream of values to JSONL bytes. Encodes each value via\n * `Schema.encodeUnknownSync`. Each line ends with `\\n`.\n */\nexport const toBytes =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.map((value) => {\n const encoded = Schema.encodeUnknownSync(schema)(value)\n return encoder.encode(JSON.stringify(encoded) + \"\\n\")\n }),\n )\n"],"mappings":";;;;;;;;;AAEA,IAAa,iBAAb,cAAoC,KAAK,YAAY,iBAAiB,CAGnE;AAMH,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;AAEH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;;;;;AAUL,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,KAAK,EACb,OAAO,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC;;;;;;AAOH,MAAa,SACJ,YACA,SACL,KAAK,KACH,OAAO,WAAW,SAChB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,KAAK;CAC3B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAM;EAAO,CAAC;CACtD,CAAC,CAAC,KACD,OAAO,SAAS,UACd,OAAO,oBAAoB,OAAO,CAAC,MAAM,CAAC,KACxC,OAAO,UAAU,UAAU,IAAI,eAAe;CAAE;CAAM;CAAO,CAAC,CAAC,CAChE,CACF,CACF,CACF,CACF;AAEL,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WACJ,YACA,SACL,KAAK,KACH,OAAO,KAAK,UAAU;CACpB,MAAM,UAAU,OAAO,kBAAkB,OAAO,CAAC,MAAM;AACvD,QAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,GAAG,KAAK;EACrD,CACH"}
1
+ {"version":3,"file":"JSONL.mjs","names":[],"sources":["../../src/streaming/JSONL.ts"],"sourcesContent":["import { Data, Effect, Schema, Stream } from \"effect\"\n\nexport class JsonParseError extends Data.TaggedError(\"JsonParseError\")<{\n readonly line: string\n readonly cause: unknown\n}> {}\n\n// ---------------------------------------------------------------------------\n// Generic stream helpers (kept module-local; see SSE.ts for the same shape).\n// ---------------------------------------------------------------------------\n\nconst decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): TextDecoder => new TextDecoder(\"utf-8\"),\n (decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,\n {\n onHalt: (decoder: TextDecoder) => {\n const tail = decoder.decode()\n return tail.length > 0 ? [tail] : []\n },\n },\n ),\n )\n\nconst splitOn =\n (separator: string) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>\n self.pipe(\n Stream.mapAccum(\n (): string => \"\",\n (buffer, chunk: string) => {\n const parts = (buffer + chunk).split(separator)\n const tail = parts[parts.length - 1] ?? \"\"\n return [tail, parts.slice(0, -1)] as const\n },\n { onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },\n ),\n )\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited\n * lines. Empty lines are skipped. Buffers across chunk boundaries.\n */\nexport const fromBytes = <E, R>(\n self: Stream.Stream<Uint8Array, E, R>,\n): Stream.Stream<string, E, R> =>\n self.pipe(\n decodeText,\n Stream.map((s) => s.replace(/\\r/g, \"\")),\n splitOn(\"\\n\"),\n Stream.filter((line) => line.length > 0),\n )\n\n/**\n * Validate each JSONL line against a Schema. JSON parse errors and Schema\n * decode errors both surface as a `JsonParseError` so callers can `catchTag`\n * uniformly.\n */\nexport const parse =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<A, JsonParseError | E, R> =>\n self.pipe(\n Stream.mapEffect((line) =>\n Effect.try({\n try: () => JSON.parse(line) as unknown,\n catch: (cause) => new JsonParseError({ line, cause }),\n }).pipe(\n Effect.flatMap((value) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError((cause) => new JsonParseError({ line, cause })),\n ),\n ),\n ),\n ),\n )\n\n/**\n * Best-effort parse of a single JSON frame. Returns the parsed value or\n * `undefined` on malformed input. Realtime WS adapters use this to skip\n * non-JSON or partially-received frames silently rather than fail the\n * entire session over one bad frame.\n */\nexport const parseSafe = (raw: string) =>\n Effect.try({\n try: () => JSON.parse(raw) as unknown,\n catch: () => undefined,\n }).pipe(Effect.orElseSucceed(() => undefined))\n\nconst encoder = new TextEncoder()\n\n/**\n * Serialize a stream of values to JSONL bytes. Encodes each value via\n * `Schema.encodeUnknownSync`. Each line ends with `\\n`.\n */\nexport const toBytes =\n <A, I>(schema: Schema.Codec<A, I>) =>\n <E, R>(self: Stream.Stream<A, E, R>): Stream.Stream<Uint8Array, E, R> =>\n self.pipe(\n Stream.map((value) => {\n const encoded = Schema.encodeUnknownSync(schema)(value)\n return encoder.encode(JSON.stringify(encoded) + \"\\n\")\n }),\n )\n"],"mappings":";;;;;;;;;;AAEA,IAAa,iBAAb,cAAoC,KAAK,YAAY,iBAAiB,CAGnE;AAMH,MAAM,cAAoB,SACxB,KAAK,KACH,OAAO,eACc,IAAI,YAAY,QAAQ,GAC1C,SAAS,UAAsB,CAAC,SAAS,CAAC,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,EACpF,EACE,SAAS,YAAyB;CAChC,MAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAO,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE;GAEvC,CACF,CACF;AAEH,MAAM,WACH,eACM,SACL,KAAK,KACH,OAAO,eACS,KACb,QAAQ,UAAkB;CACzB,MAAM,SAAS,SAAS,OAAO,MAAM,UAAU;AAE/C,QAAO,CADM,MAAM,MAAM,SAAS,MAAM,IAC1B,MAAM,MAAM,GAAG,GAAG,CAAC;GAEnC,EAAE,SAAS,SAAkB,KAAK,SAAS,IAAI,CAAC,KAAK,GAAG,EAAE,EAAG,CAC9D,CACF;;;;;AAUL,MAAa,aACX,SAEA,KAAK,KACH,YACA,OAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,QAAQ,KAAK,EACb,OAAO,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC;;;;;;AAOH,MAAa,SACJ,YACA,SACL,KAAK,KACH,OAAO,WAAW,SAChB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,KAAK;CAC3B,QAAQ,UAAU,IAAI,eAAe;EAAE;EAAM;EAAO,CAAC;CACtD,CAAC,CAAC,KACD,OAAO,SAAS,UACd,OAAO,oBAAoB,OAAO,CAAC,MAAM,CAAC,KACxC,OAAO,UAAU,UAAU,IAAI,eAAe;CAAE;CAAM;CAAO,CAAC,CAAC,CAChE,CACF,CACF,CACF,CACF;;;;;;;AAQL,MAAa,aAAa,QACxB,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,IAAI;CAC1B,aAAa,KAAA;CACd,CAAC,CAAC,KAAK,OAAO,oBAAoB,KAAA,EAAU,CAAC;AAEhD,MAAM,UAAU,IAAI,aAAa;;;;;AAMjC,MAAa,WACJ,YACA,SACL,KAAK,KACH,OAAO,KAAK,UAAU;CACpB,MAAM,UAAU,OAAO,kBAAkB,OAAO,CAAC,MAAM;AACvD,QAAO,QAAQ,OAAO,KAAK,UAAU,QAAQ,GAAG,KAAK;EACrD,CACH"}