@effect-uai/core 0.5.2 → 0.7.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 (175) hide show
  1. package/README.md +1 -1
  2. package/dist/{Audio-BfCTGnH3.d.mts → Audio-DPlTsNdz.d.mts} +3 -2
  3. package/dist/Audio-DPlTsNdz.d.mts.map +1 -0
  4. package/dist/{Items-BH8xUkoR.d.mts → Items-D8UaXQx_.d.mts} +15 -15
  5. package/dist/Items-D8UaXQx_.d.mts.map +1 -0
  6. package/dist/SandboxError-BvKypkJm.d.mts +158 -0
  7. package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
  8. package/dist/{Tool-87ViKCCO.d.mts → Tool-CiFXYjmR.d.mts} +11 -11
  9. package/dist/{Tool-87ViKCCO.d.mts.map → Tool-CiFXYjmR.d.mts.map} +1 -1
  10. package/dist/capabilities/Capabilities.d.mts +78 -0
  11. package/dist/capabilities/Capabilities.d.mts.map +1 -0
  12. package/dist/capabilities/Capabilities.mjs +65 -0
  13. package/dist/capabilities/Capabilities.mjs.map +1 -0
  14. package/dist/domain/Audio.d.mts +1 -1
  15. package/dist/domain/Audio.mjs.map +1 -1
  16. package/dist/domain/Items.d.mts +2 -2
  17. package/dist/domain/Items.mjs +15 -15
  18. package/dist/domain/Items.mjs.map +1 -1
  19. package/dist/domain/Music.d.mts +131 -72
  20. package/dist/domain/Music.d.mts.map +1 -1
  21. package/dist/domain/Music.mjs +36 -9
  22. package/dist/domain/Music.mjs.map +1 -1
  23. package/dist/domain/Transcript.d.mts +8 -2
  24. package/dist/domain/Transcript.d.mts.map +1 -1
  25. package/dist/domain/Transcript.mjs.map +1 -1
  26. package/dist/domain/Turn.d.mts +388 -2
  27. package/dist/domain/Turn.d.mts.map +1 -0
  28. package/dist/domain/Turn.mjs +13 -13
  29. package/dist/domain/Turn.mjs.map +1 -1
  30. package/dist/embedding-model/Embedding.d.mts +12 -2
  31. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  32. package/dist/embedding-model/Embedding.mjs.map +1 -1
  33. package/dist/embedding-model/EmbeddingModel.d.mts +36 -23
  34. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  35. package/dist/embedding-model/EmbeddingModel.mjs +20 -1
  36. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  37. package/dist/embedding-model/EmbeddingModel.test.mjs +15 -3
  38. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  39. package/dist/index.d.mts +13 -7
  40. package/dist/index.mjs +9 -3
  41. package/dist/language-model/LanguageModel.d.mts +5 -5
  42. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  43. package/dist/language-model/LanguageModel.mjs.map +1 -1
  44. package/dist/loop/Loop.d.mts +37 -90
  45. package/dist/loop/Loop.d.mts.map +1 -1
  46. package/dist/loop/Loop.mjs +39 -93
  47. package/dist/loop/Loop.mjs.map +1 -1
  48. package/dist/loop/Loop.test.mjs +46 -54
  49. package/dist/loop/Loop.test.mjs.map +1 -1
  50. package/dist/music-generator/MusicGenerator.d.mts +48 -25
  51. package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
  52. package/dist/music-generator/MusicGenerator.mjs +17 -9
  53. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  54. package/dist/music-generator/MusicGenerator.test.mjs +81 -56
  55. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
  56. package/dist/sandbox/Memory.d.mts +53 -0
  57. package/dist/sandbox/Memory.d.mts.map +1 -0
  58. package/dist/sandbox/Memory.mjs +68 -0
  59. package/dist/sandbox/Memory.mjs.map +1 -0
  60. package/dist/sandbox/Sandbox.d.mts +717 -0
  61. package/dist/sandbox/Sandbox.d.mts.map +1 -0
  62. package/dist/sandbox/Sandbox.mjs +192 -0
  63. package/dist/sandbox/Sandbox.mjs.map +1 -0
  64. package/dist/sandbox/SandboxError.d.mts +2 -0
  65. package/dist/sandbox/SandboxError.mjs +89 -0
  66. package/dist/sandbox/SandboxError.mjs.map +1 -0
  67. package/dist/sandbox/SandboxImage.d.mts +28 -0
  68. package/dist/sandbox/SandboxImage.d.mts.map +1 -0
  69. package/dist/sandbox/SandboxImage.mjs +32 -0
  70. package/dist/sandbox/SandboxImage.mjs.map +1 -0
  71. package/dist/sandbox/SandboxNetwork.d.mts +34 -0
  72. package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
  73. package/dist/sandbox/SandboxNetwork.mjs +36 -0
  74. package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
  75. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +84 -3
  76. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
  77. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
  78. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  79. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +59 -5
  80. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
  81. package/dist/testing/MockMusicGenerator.d.mts +12 -7
  82. package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
  83. package/dist/testing/MockMusicGenerator.mjs +4 -3
  84. package/dist/testing/MockMusicGenerator.mjs.map +1 -1
  85. package/dist/testing/MockProvider.d.mts +3 -3
  86. package/dist/testing/MockProvider.d.mts.map +1 -1
  87. package/dist/testing/MockProvider.mjs.map +1 -1
  88. package/dist/testing/MockSandbox.d.mts +604 -0
  89. package/dist/testing/MockSandbox.d.mts.map +1 -0
  90. package/dist/testing/MockSandbox.mjs +160 -0
  91. package/dist/testing/MockSandbox.mjs.map +1 -0
  92. package/dist/testing/MockSpeechSynthesizer.d.mts +27 -12
  93. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  94. package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
  95. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  96. package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
  97. package/dist/tool/Approval.d.mts.map +1 -0
  98. package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
  99. package/dist/tool/Approval.mjs.map +1 -0
  100. package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
  101. package/dist/tool/Approval.test.mjs.map +1 -0
  102. package/dist/tool/HistoryCheck.d.mts +6 -6
  103. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  104. package/dist/tool/HistoryCheck.mjs +5 -5
  105. package/dist/tool/HistoryCheck.mjs.map +1 -1
  106. package/dist/tool/Tool.d.mts +2 -2
  107. package/dist/tool/Tool.mjs +4 -4
  108. package/dist/tool/Tool.mjs.map +1 -1
  109. package/dist/tool/ToolEvent.d.mts +19 -19
  110. package/dist/tool/ToolEvent.d.mts.map +1 -1
  111. package/dist/tool/ToolEvent.mjs +8 -8
  112. package/dist/tool/ToolEvent.mjs.map +1 -1
  113. package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
  114. package/dist/tool/ToolResult.d.mts.map +1 -0
  115. package/dist/tool/ToolResult.mjs +61 -0
  116. package/dist/tool/ToolResult.mjs.map +1 -0
  117. package/dist/tool/Toolkit.d.mts +58 -26
  118. package/dist/tool/Toolkit.d.mts.map +1 -1
  119. package/dist/tool/Toolkit.mjs +62 -34
  120. package/dist/tool/Toolkit.mjs.map +1 -1
  121. package/dist/tool/Toolkit.test.mjs +13 -13
  122. package/dist/tool/Toolkit.test.mjs.map +1 -1
  123. package/dist/transcriber/Transcriber.d.mts +11 -10
  124. package/dist/transcriber/Transcriber.d.mts.map +1 -1
  125. package/dist/transcriber/Transcriber.mjs.map +1 -1
  126. package/dist/transcriber/Transcriber.test.mjs +3 -3
  127. package/dist/transcriber/Transcriber.test.mjs.map +1 -1
  128. package/package.json +35 -7
  129. package/src/capabilities/Capabilities.ts +81 -0
  130. package/src/domain/Audio.ts +2 -1
  131. package/src/domain/Items.ts +9 -9
  132. package/src/domain/Music.ts +142 -68
  133. package/src/domain/Transcript.ts +7 -1
  134. package/src/domain/Turn.ts +16 -16
  135. package/src/embedding-model/Embedding.ts +11 -0
  136. package/src/embedding-model/EmbeddingModel.test.ts +15 -4
  137. package/src/embedding-model/EmbeddingModel.ts +48 -21
  138. package/src/index.ts +8 -2
  139. package/src/language-model/LanguageModel.ts +3 -3
  140. package/src/loop/Loop.test.ts +93 -91
  141. package/src/loop/Loop.ts +72 -170
  142. package/src/music-generator/MusicGenerator.test.ts +78 -53
  143. package/src/music-generator/MusicGenerator.ts +65 -24
  144. package/src/sandbox/Memory.ts +128 -0
  145. package/src/sandbox/Sandbox.ts +639 -0
  146. package/src/sandbox/SandboxError.ts +140 -0
  147. package/src/sandbox/SandboxImage.ts +26 -0
  148. package/src/sandbox/SandboxNetwork.ts +34 -0
  149. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +92 -3
  150. package/src/speech-synthesizer/SpeechSynthesizer.ts +109 -0
  151. package/src/testing/MockMusicGenerator.ts +28 -18
  152. package/src/testing/MockProvider.ts +21 -20
  153. package/src/testing/MockSandbox.ts +342 -0
  154. package/src/testing/MockSpeechSynthesizer.ts +137 -77
  155. package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
  156. package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
  157. package/src/tool/HistoryCheck.ts +10 -13
  158. package/src/tool/Tool.ts +13 -15
  159. package/src/tool/ToolEvent.ts +7 -7
  160. package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
  161. package/src/tool/Toolkit.test.ts +13 -13
  162. package/src/tool/Toolkit.ts +97 -73
  163. package/src/transcriber/Transcriber.test.ts +3 -3
  164. package/src/transcriber/Transcriber.ts +10 -7
  165. package/dist/Audio-BfCTGnH3.d.mts.map +0 -1
  166. package/dist/Items-BH8xUkoR.d.mts.map +0 -1
  167. package/dist/Turn-0CwCAyVe.d.mts +0 -388
  168. package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
  169. package/dist/tool/Outcome.d.mts.map +0 -1
  170. package/dist/tool/Outcome.mjs +0 -61
  171. package/dist/tool/Outcome.mjs.map +0 -1
  172. package/dist/tool/Resolvers.d.mts.map +0 -1
  173. package/dist/tool/Resolvers.mjs.map +0 -1
  174. package/dist/tool/Resolvers.test.mjs.map +0 -1
  175. /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
@@ -1,11 +1,26 @@
1
1
  import { t as AiError } from "../AiError-CAX_48RU.mjs";
2
- import { n as AudioChunk, r as AudioFormat, t as AudioBlob } from "../Audio-BfCTGnH3.mjs";
2
+ import { n as AudioChunk, r as AudioFormat, t as AudioBlob } from "../Audio-DPlTsNdz.mjs";
3
3
  import { Context, Effect, Stream } from "effect";
4
4
 
5
5
  //#region src/speech-synthesizer/SpeechSynthesizer.d.ts
6
6
  declare namespace SpeechSynthesizer_d_exports {
7
- export { CommonStreamSynthesizeRequest, CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize };
7
+ export { CommonStreamSynthesizeRequest, CommonSynthesizeDialogueRequest, CommonSynthesizeRequest, CustomPronunciation, DialogueTurn, MultiSpeakerTts, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue };
8
8
  }
9
+ /**
10
+ * Per-phrase pronunciation override. `pronunciation` is always IPA, the
11
+ * universal phonetic interchange format. Adapters translate to the
12
+ * provider's wire form (inline `/ipa/` token, SSML `<phoneme>`, or a
13
+ * structured field), converting IPA to X-SAMPA internally where the wire
14
+ * requires it (Google, Azure).
15
+ *
16
+ * Pronunciations are load-bearing (the configured word is mispronounced
17
+ * if dropped), so a provider with no stateless IPA path fails the call
18
+ * with `AiError.Unsupported` rather than silently degrading.
19
+ */
20
+ type CustomPronunciation = {
21
+ readonly phrase: string; /** IPA notation, e.g. `"ænˈθrɒpɪk"`. */
22
+ readonly pronunciation: string;
23
+ };
9
24
  /**
10
25
  * Cross-provider synthesis request. Provider-specific extensions
11
26
  * (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,
@@ -26,6 +41,13 @@ type CommonSynthesizeRequest = {
26
41
  readonly outputFormat?: AudioFormat;
27
42
  readonly speed?: number;
28
43
  readonly languageCode?: string;
44
+ /**
45
+ * IPA phoneme overrides for specific phrases. Adapters render them to
46
+ * the provider's wire form (inline token, SSML `<phoneme>`, structured
47
+ * field). Load-bearing: a provider with no stateless IPA path fails
48
+ * the call with `AiError.Unsupported` rather than mispronouncing.
49
+ */
50
+ readonly pronunciations?: ReadonlyArray<CustomPronunciation>;
29
51
  };
30
52
  /**
31
53
  * Incremental-synthesis request — text arrives as `Stream<string>`.
@@ -37,6 +59,29 @@ type CommonSynthesizeRequest = {
37
59
  * call. Provider extensions can expose `forkContext` for that.
38
60
  */
39
61
  type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, "text">;
62
+ /**
63
+ * One turn in a multi-speaker dialogue. Just a voice and its line; the
64
+ * only in-tree dialogue endpoint (ElevenLabs `/v1/text-to-dialogue`)
65
+ * accepts `{ voice_id, text }` per turn. Per-turn styling knobs
66
+ * (Hume Octave-2 `styleDescription` / `speed`) are provider-specific
67
+ * and belong on a provider-typed turn extension, not here.
68
+ */
69
+ type DialogueTurn = {
70
+ readonly voiceId: string;
71
+ readonly text: string;
72
+ };
73
+ /**
74
+ * Cross-provider dialogue request. Same return type as `synthesize` —
75
+ * one continuous `AudioBlob`. Per-turn timing metadata is not exposed
76
+ * in the common shape (only Hume returns it natively).
77
+ */
78
+ type CommonSynthesizeDialogueRequest = {
79
+ readonly model: string;
80
+ readonly turns: ReadonlyArray<DialogueTurn>;
81
+ readonly outputFormat?: AudioFormat;
82
+ readonly languageCode?: string;
83
+ readonly pronunciations?: ReadonlyArray<CustomPronunciation>;
84
+ };
40
85
  type SpeechSynthesizerService = {
41
86
  /** One-shot. Full text in, full audio bytes out. Universally supported. */readonly synthesize: (request: CommonSynthesizeRequest) => Effect.Effect<AudioBlob, AiError>;
42
87
  /**
@@ -55,6 +100,22 @@ type SpeechSynthesizerService = {
55
100
  * error.
56
101
  */
57
102
  readonly streamSynthesisFrom: <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) => Stream.Stream<AudioChunk, AiError | E, R>;
103
+ /**
104
+ * One-shot multi-speaker dialogue. Turn array in, single `AudioBlob`
105
+ * out. Providers without native dialogue support return
106
+ * `AiError.Unsupported` and do NOT ship the `MultiSpeakerTts` marker
107
+ * — calls via the top-level helper become a compile-time error.
108
+ */
109
+ readonly synthesizeDialogue: (request: CommonSynthesizeDialogueRequest) => Effect.Effect<AudioBlob, AiError>;
110
+ /**
111
+ * Chunked-streaming variant of `synthesizeDialogue`. Same input,
112
+ * audio chunks emitted as the wire delivers them. Providers that
113
+ * synthesize the whole dialogue server-side may fall back to a
114
+ * single-chunk wrap of the sync result.
115
+ *
116
+ * Gated by `MultiSpeakerTts` at the top-level helper.
117
+ */
118
+ readonly streamSynthesizeDialogue: (request: CommonSynthesizeDialogueRequest) => Stream.Stream<AudioChunk, AiError>;
58
119
  };
59
120
  declare const SpeechSynthesizer_base: Context.ServiceClass<SpeechSynthesizer, "@betalyra/effect-uai/SpeechSynthesizer", SpeechSynthesizerService>;
60
121
  declare class SpeechSynthesizer extends SpeechSynthesizer_base {}
@@ -70,6 +131,17 @@ declare const TtsIncrementalText_base: Context.ServiceClass<TtsIncrementalText,
70
131
  * `Layer.succeed(TtsIncrementalText, undefined)`.
71
132
  */
72
133
  declare class TtsIncrementalText extends TtsIncrementalText_base {}
134
+ declare const MultiSpeakerTts_base: Context.ServiceClass<MultiSpeakerTts, "@betalyra/effect-uai/capability/MultiSpeakerTts", void>;
135
+ /**
136
+ * Capability marker for multi-speaker dialogue. Shipped by provider
137
+ * Layers whose `synthesizeDialogue` is wired to a native multi-speaker
138
+ * endpoint (ElevenLabs `/v1/text-to-dialogue`, Hume `/v0/tts` with
139
+ * `utterances[]`, Google Cloud TTS Gemini TTS `multiSpeakerMarkup`).
140
+ * Other providers leave the marker unregistered so the top-level
141
+ * `synthesizeDialogue` / `streamSynthesizeDialogue` helpers fail to
142
+ * satisfy R against those Layers — compile-time error.
143
+ */
144
+ declare class MultiSpeakerTts extends MultiSpeakerTts_base {}
73
145
  /** One-shot synthesis. */
74
146
  declare const synthesize: (request: CommonSynthesizeRequest) => Effect.Effect<AudioBlob, AiError, SpeechSynthesizer>;
75
147
  /** Full text in, audio chunks out. */
@@ -91,6 +163,15 @@ declare const streamSynthesisFrom: {
91
163
  (request: CommonStreamSynthesizeRequest): <E, R>(textIn: Stream.Stream<string, E, R>) => Stream.Stream<AudioChunk, AiError | E, R | SpeechSynthesizer | TtsIncrementalText>;
92
164
  <E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest): Stream.Stream<AudioChunk, AiError | E, R | SpeechSynthesizer | TtsIncrementalText>;
93
165
  };
166
+ /**
167
+ * One-shot multi-speaker dialogue. Requires `MultiSpeakerTts` in R —
168
+ * providers without dialogue support are a compile-time error.
169
+ */
170
+ declare const synthesizeDialogue: (request: CommonSynthesizeDialogueRequest) => Effect.Effect<AudioBlob, AiError, SpeechSynthesizer | MultiSpeakerTts>;
171
+ /**
172
+ * Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.
173
+ */
174
+ declare const streamSynthesizeDialogue: (request: CommonSynthesizeDialogueRequest) => Stream.Stream<AudioChunk, AiError, SpeechSynthesizer | MultiSpeakerTts>;
94
175
  //#endregion
95
- export { CommonStreamSynthesizeRequest, CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize, SpeechSynthesizer_d_exports as t };
176
+ export { CommonStreamSynthesizeRequest, CommonSynthesizeDialogueRequest, CommonSynthesizeRequest, CustomPronunciation, DialogueTurn, MultiSpeakerTts, SpeechSynthesizer, SpeechSynthesizerService, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue, SpeechSynthesizer_d_exports as t };
96
177
  //# sourceMappingURL=SpeechSynthesizer.d.mts.map
@@ -1 +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"}
1
+ {"version":3,"file":"SpeechSynthesizer.d.mts","names":[],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAeY,mBAAA;EAAA,SACD,MAAA;WAEA,aAAA;AAAA;;;;AAHX;;;;KAaY,uBAAA;EAAA,SACD,IAAA,UADwB;EAAA,SAGxB,KAAA;EAQe;;;;;;EAAA,SADf,OAAA;EAAA,SACA,YAAA,GAAe,WAAA;EAAA,SACf,KAAA;EAAA,SACA,YAAA;EADA;;;;;;EAAA,SAQA,cAAA,GAAiB,aAAA,CAAc,mBAAA;AAAA;;;;;AAqB1C;;;;;KATY,6BAAA,GAAgC,IAAA,CAAK,uBAAA;;;;;;;;KASrC,YAAA;EAAA,SACD,OAAA;EAAA,SACA,IAAA;AAAA;;;;;;KAQC,+BAAA;EAAA,SACD,KAAA;EAAA,SACA,KAAA,EAAO,aAAA,CAAc,YAAA;EAAA,SACrB,YAAA,GAAe,WAAA;EAAA,SACf,YAAA;EAAA,SACA,cAAA,GAAiB,aAAA,CAAc,mBAAA;AAAA;AAAA,KAG9B,wBAAA;EAGC,oFADF,UAAA,GACP,OAAA,EAAS,uBAAA,KACN,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA;EAAA;;;;EAAA,SAKrB,eAAA,GACP,OAAA,EAAS,uBAAA,KACN,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA;EAA1B;;;;;;;;;;EAAA,SAWI,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;EASjC;;;;;;EAAA,SAFV,kBAAA,GACP,OAAA,EAAS,+BAAA,KACN,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA;EAWZ;;;;;;;;EAAA,SAFT,wBAAA,GACP,OAAA,EAAS,+BAAA,KACN,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA;AAAA;AAAA,cAChC,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,cAGS,uBAAA;;;;;;;;;;;cAYnC,kBAAA,SAA2B,uBAAA;AAAA,cAEpC,oBAAA;;;;;;;;;;cAWS,eAAA,SAAwB,oBAAA;;cAKxB,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;;;;;;;;;;;;AA3C7C;;cA2DY,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;;;;;cAiB/D,kBAAA,GACX,OAAA,EAAS,+BAAA,KACR,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,EAAiB,iBAAA,GAAoB,eAAA;;;;cAUpD,wBAAA,GACX,OAAA,EAAS,+BAAA,KACR,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,EAAiB,iBAAA,GAAoB,eAAA"}
@@ -2,11 +2,14 @@ import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
2
  import { Context, Effect, Function, Stream } from "effect";
3
3
  //#region src/speech-synthesizer/SpeechSynthesizer.ts
4
4
  var SpeechSynthesizer_exports = /* @__PURE__ */ __exportAll({
5
+ MultiSpeakerTts: () => MultiSpeakerTts,
5
6
  SpeechSynthesizer: () => SpeechSynthesizer,
6
7
  TtsIncrementalText: () => TtsIncrementalText,
7
8
  streamSynthesis: () => streamSynthesis,
8
9
  streamSynthesisFrom: () => streamSynthesisFrom,
9
- synthesize: () => synthesize
10
+ streamSynthesizeDialogue: () => streamSynthesizeDialogue,
11
+ synthesize: () => synthesize,
12
+ synthesizeDialogue: () => synthesizeDialogue
10
13
  });
11
14
  var SpeechSynthesizer = class extends Context.Service()("@betalyra/effect-uai/SpeechSynthesizer") {};
12
15
  /**
@@ -20,6 +23,16 @@ var SpeechSynthesizer = class extends Context.Service()("@betalyra/effect-uai/Sp
20
23
  * `Layer.succeed(TtsIncrementalText, undefined)`.
21
24
  */
22
25
  var TtsIncrementalText = class extends Context.Service()("@betalyra/effect-uai/capability/TtsIncrementalText") {};
26
+ /**
27
+ * Capability marker for multi-speaker dialogue. Shipped by provider
28
+ * Layers whose `synthesizeDialogue` is wired to a native multi-speaker
29
+ * endpoint (ElevenLabs `/v1/text-to-dialogue`, Hume `/v0/tts` with
30
+ * `utterances[]`, Google Cloud TTS Gemini TTS `multiSpeakerMarkup`).
31
+ * Other providers leave the marker unregistered so the top-level
32
+ * `synthesizeDialogue` / `streamSynthesizeDialogue` helpers fail to
33
+ * satisfy R against those Layers — compile-time error.
34
+ */
35
+ var MultiSpeakerTts = class extends Context.Service()("@betalyra/effect-uai/capability/MultiSpeakerTts") {};
23
36
  /** One-shot synthesis. */
24
37
  const synthesize = (request) => Effect.flatMap(SpeechSynthesizer.asEffect(), (s) => s.synthesize(request));
25
38
  /** Full text in, audio chunks out. */
@@ -42,7 +55,24 @@ const streamSynthesisFrom = Function.dual(2, (textIn, request) => Stream.unwrap(
42
55
  yield* TtsIncrementalText.asEffect();
43
56
  return s.streamSynthesisFrom(textIn, request);
44
57
  })));
58
+ /**
59
+ * One-shot multi-speaker dialogue. Requires `MultiSpeakerTts` in R —
60
+ * providers without dialogue support are a compile-time error.
61
+ */
62
+ const synthesizeDialogue = (request) => Effect.gen(function* () {
63
+ const s = yield* SpeechSynthesizer.asEffect();
64
+ yield* MultiSpeakerTts.asEffect();
65
+ return yield* s.synthesizeDialogue(request);
66
+ });
67
+ /**
68
+ * Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.
69
+ */
70
+ const streamSynthesizeDialogue = (request) => Stream.unwrap(Effect.gen(function* () {
71
+ const s = yield* SpeechSynthesizer.asEffect();
72
+ yield* MultiSpeakerTts.asEffect();
73
+ return s.streamSynthesizeDialogue(request);
74
+ }));
45
75
  //#endregion
46
- export { SpeechSynthesizer, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, synthesize, SpeechSynthesizer_exports as t };
76
+ export { MultiSpeakerTts, SpeechSynthesizer, TtsIncrementalText, streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue, SpeechSynthesizer_exports as t };
47
77
 
48
78
  //# sourceMappingURL=SpeechSynthesizer.mjs.map
@@ -1 +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"}
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 * Per-phrase pronunciation override. `pronunciation` is always IPA, the\n * universal phonetic interchange format. Adapters translate to the\n * provider's wire form (inline `/ipa/` token, SSML `<phoneme>`, or a\n * structured field), converting IPA to X-SAMPA internally where the wire\n * requires it (Google, Azure).\n *\n * Pronunciations are load-bearing (the configured word is mispronounced\n * if dropped), so a provider with no stateless IPA path fails the call\n * with `AiError.Unsupported` rather than silently degrading.\n */\nexport type CustomPronunciation = {\n readonly phrase: string\n /** IPA notation, e.g. `\"ænˈθrɒpɪk\"`. */\n readonly pronunciation: string\n}\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 * IPA phoneme overrides for specific phrases. Adapters render them to\n * the provider's wire form (inline token, SSML `<phoneme>`, structured\n * field). Load-bearing: a provider with no stateless IPA path fails\n * the call with `AiError.Unsupported` rather than mispronouncing.\n */\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\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\n/**\n * One turn in a multi-speaker dialogue. Just a voice and its line; the\n * only in-tree dialogue endpoint (ElevenLabs `/v1/text-to-dialogue`)\n * accepts `{ voice_id, text }` per turn. Per-turn styling knobs\n * (Hume Octave-2 `styleDescription` / `speed`) are provider-specific\n * and belong on a provider-typed turn extension, not here.\n */\nexport type DialogueTurn = {\n readonly voiceId: string\n readonly text: string\n}\n\n/**\n * Cross-provider dialogue request. Same return type as `synthesize` —\n * one continuous `AudioBlob`. Per-turn timing metadata is not exposed\n * in the common shape (only Hume returns it natively).\n */\nexport type CommonSynthesizeDialogueRequest = {\n readonly model: string\n readonly turns: ReadonlyArray<DialogueTurn>\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\n}\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 * One-shot multi-speaker dialogue. Turn array in, single `AudioBlob`\n * out. Providers without native dialogue support return\n * `AiError.Unsupported` and do NOT ship the `MultiSpeakerTts` marker\n * — calls via the top-level helper become a compile-time error.\n */\n readonly synthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Effect.Effect<AudioBlob, AiError.AiError>\n /**\n * Chunked-streaming variant of `synthesizeDialogue`. Same input,\n * audio chunks emitted as the wire delivers them. Providers that\n * synthesize the whole dialogue server-side may fall back to a\n * single-chunk wrap of the sync result.\n *\n * Gated by `MultiSpeakerTts` at the top-level helper.\n */\n readonly streamSynthesizeDialogue: (\n request: CommonSynthesizeDialogueRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\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/**\n * Capability marker for multi-speaker dialogue. Shipped by provider\n * Layers whose `synthesizeDialogue` is wired to a native multi-speaker\n * endpoint (ElevenLabs `/v1/text-to-dialogue`, Hume `/v0/tts` with\n * `utterances[]`, Google Cloud TTS Gemini TTS `multiSpeakerMarkup`).\n * Other providers leave the marker unregistered so the top-level\n * `synthesizeDialogue` / `streamSynthesizeDialogue` helpers fail to\n * satisfy R against those Layers — compile-time error.\n */\nexport class MultiSpeakerTts extends Context.Service<MultiSpeakerTts, void>()(\n \"@betalyra/effect-uai/capability/MultiSpeakerTts\",\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\n/**\n * One-shot multi-speaker dialogue. Requires `MultiSpeakerTts` in R —\n * providers without dialogue support are a compile-time error.\n */\nexport const synthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* MultiSpeakerTts.asEffect()\n return yield* s.synthesizeDialogue(request)\n })\n\n/**\n * Chunked-streaming variant. Same R requirement as `synthesizeDialogue`.\n */\nexport const streamSynthesizeDialogue = (\n request: CommonSynthesizeDialogueRequest,\n): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const s = yield* SpeechSynthesizer.asEffect()\n yield* MultiSpeakerTts.asEffect()\n return s.streamSynthesizeDialogue(request)\n }),\n )\n"],"mappings":";;;;;;;;;;;;;AAuIA,IAAa,oBAAb,cAAuC,QAAQ,SAG5C,CAAC,yCAAyC,CAAC;;;;;;;;;;;AAY9C,IAAa,qBAAb,cAAwC,QAAQ,SAAmC,CACjF,qDACD,CAAC;;;;;;;;;;AAWF,IAAa,kBAAb,cAAqC,QAAQ,SAAgC,CAC3E,kDACD,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;;;;;AAMD,MAAa,sBACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,gBAAgB,UAAU;AACjC,QAAO,OAAO,EAAE,mBAAmB,QAAQ;EAC3C;;;;AAKJ,MAAa,4BACX,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,IAAI,OAAO,kBAAkB,UAAU;AAC7C,QAAO,gBAAgB,UAAU;AACjC,QAAO,EAAE,yBAAyB,QAAQ;EAC1C,CACH"}
@@ -1,7 +1,7 @@
1
- import { streamSynthesis, streamSynthesisFrom, synthesize } from "./SpeechSynthesizer.mjs";
1
+ import { streamSynthesis, streamSynthesisFrom, streamSynthesizeDialogue, synthesize, synthesizeDialogue } from "./SpeechSynthesizer.mjs";
2
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";
3
+ import { layer, layerWithoutIncremental, layerWithoutMultiSpeaker } from "../testing/MockSpeechSynthesizer.mjs";
4
+ import { Duration, Effect, Stream } from "effect";
5
5
  //#region src/speech-synthesizer/SpeechSynthesizer.test.ts
6
6
  const blob = {
7
7
  format: {
@@ -15,7 +15,7 @@ const blob = {
15
15
  190,
16
16
  239
17
17
  ]),
18
- durationSeconds: .5
18
+ duration: Duration.millis(500)
19
19
  };
20
20
  const chunk = (n) => ({ bytes: new Uint8Array([n]) });
21
21
  describe("SpeechSynthesizer.synthesize", () => {
@@ -28,7 +28,7 @@ describe("SpeechSynthesizer.synthesize", () => {
28
28
  });
29
29
  const result = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
30
30
  globalExpect(result.bytes).toEqual(blob.bytes);
31
- globalExpect(result.durationSeconds).toBe(.5);
31
+ globalExpect(result.duration).toEqual(Duration.millis(500));
32
32
  });
33
33
  });
34
34
  describe("SpeechSynthesizer.streamSynthesis", () => {
@@ -83,6 +83,60 @@ describe("SpeechSynthesizer capability marker (compile-time)", () => {
83
83
  (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(fullMock.layer))).toEqualTypeOf();
84
84
  });
85
85
  });
86
+ describe("SpeechSynthesizer.synthesizeDialogue", () => {
87
+ const dialogueReq = {
88
+ model: "mock-tts",
89
+ turns: [{
90
+ voiceId: "voice-a",
91
+ text: "Hi"
92
+ }, {
93
+ voiceId: "voice-b",
94
+ text: "Hello"
95
+ }]
96
+ };
97
+ it("returns the scripted AudioBlob", async () => {
98
+ const mock = layer({ dialogueBlobs: [blob] });
99
+ globalExpect((await Effect.runPromise(synthesizeDialogue(dialogueReq).pipe(Effect.provide(mock.layer)))).bytes).toEqual(blob.bytes);
100
+ });
101
+ it("records the request on the synthesizeDialogue call channel", async () => {
102
+ const mock = layer({ dialogueBlobs: [blob] });
103
+ const program = Effect.gen(function* () {
104
+ yield* synthesizeDialogue(dialogueReq);
105
+ return yield* mock.recorder;
106
+ });
107
+ const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)));
108
+ globalExpect(rec.synthesizeDialogueCalls.length).toBe(1);
109
+ globalExpect(rec.synthesizeDialogueCalls[0].turns.length).toBe(2);
110
+ globalExpect(rec.synthesizeCalls.length).toBe(0);
111
+ });
112
+ it("emits scripted chunks for streamSynthesizeDialogue", async () => {
113
+ const mock = layer({ streamSynthesizeDialogueChunks: [[chunk(7), chunk(8)]] });
114
+ globalExpect((await Effect.runPromise(Stream.runCollect(streamSynthesizeDialogue(dialogueReq)).pipe(Effect.provide(mock.layer)))).map((c) => Array.from(c.bytes))).toEqual([[7], [8]]);
115
+ });
116
+ });
117
+ describe("MultiSpeakerTts capability marker (compile-time)", () => {
118
+ const dialogueReq = {
119
+ model: "mock-tts",
120
+ turns: [{
121
+ voiceId: "a",
122
+ text: "Hi"
123
+ }]
124
+ };
125
+ it("requires `MultiSpeakerTts` on the R channel of synthesizeDialogue", () => {
126
+ (0, import_dist.expectTypeOf)(synthesizeDialogue(dialogueReq)).toEqualTypeOf();
127
+ });
128
+ it("requires `MultiSpeakerTts` on the R channel of streamSynthesizeDialogue", () => {
129
+ (0, import_dist.expectTypeOf)(streamSynthesizeDialogue(dialogueReq)).toEqualTypeOf();
130
+ });
131
+ it("a layer without the marker leaves `MultiSpeakerTts` unsatisfied in R", () => {
132
+ const noMarker = layerWithoutMultiSpeaker({});
133
+ (0, import_dist.expectTypeOf)(synthesizeDialogue(dialogueReq).pipe(Effect.provide(noMarker.layer))).toEqualTypeOf();
134
+ });
135
+ it("a full layer (with marker) clears R to never", () => {
136
+ const fullMock = layer({ dialogueBlobs: [blob] });
137
+ (0, import_dist.expectTypeOf)(synthesizeDialogue(dialogueReq).pipe(Effect.provide(fullMock.layer))).toEqualTypeOf();
138
+ });
139
+ });
86
140
  describe("SpeechSynthesizer.streamSynthesisFrom", () => {
87
141
  const ssfReq = {
88
142
  model: "mock-tts",
@@ -1 +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":"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"}
1
+ {"version":3,"file":"SpeechSynthesizer.test.mjs","names":["MockSpeechSynthesizer.layer","SpeechSynthesizer.synthesize","SpeechSynthesizer.streamSynthesis","SpeechSynthesizer.streamSynthesisFrom","MockSpeechSynthesizer.layerWithoutIncremental","SpeechSynthesizer.synthesizeDialogue","SpeechSynthesizer.streamSynthesizeDialogue","MockSpeechSynthesizer.layerWithoutMultiSpeaker"],"sources":["../../src/speech-synthesizer/SpeechSynthesizer.test.ts"],"sourcesContent":["import { Duration, 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 duration: Duration.millis(500),\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.duration).toEqual(Duration.millis(500))\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.synthesizeDialogue\", () => {\n const dialogueReq: SpeechSynthesizer.CommonSynthesizeDialogueRequest = {\n model: \"mock-tts\",\n turns: [\n { voiceId: \"voice-a\", text: \"Hi\" },\n { voiceId: \"voice-b\", text: \"Hello\" },\n ],\n }\n\n it(\"returns the scripted AudioBlob\", async () => {\n const mock = MockSpeechSynthesizer.layer({ dialogueBlobs: [blob] })\n const result = await Effect.runPromise(\n SpeechSynthesizer.synthesizeDialogue(dialogueReq).pipe(Effect.provide(mock.layer)),\n )\n expect(result.bytes).toEqual(blob.bytes)\n })\n\n it(\"records the request on the synthesizeDialogue call channel\", async () => {\n const mock = MockSpeechSynthesizer.layer({ dialogueBlobs: [blob] })\n const program = Effect.gen(function* () {\n yield* SpeechSynthesizer.synthesizeDialogue(dialogueReq)\n return yield* mock.recorder\n })\n const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))\n expect(rec.synthesizeDialogueCalls.length).toBe(1)\n expect(rec.synthesizeDialogueCalls[0]!.turns.length).toBe(2)\n expect(rec.synthesizeCalls.length).toBe(0)\n })\n\n it(\"emits scripted chunks for streamSynthesizeDialogue\", async () => {\n const mock = MockSpeechSynthesizer.layer({\n streamSynthesizeDialogueChunks: [[chunk(7), chunk(8)]],\n })\n const out = await Effect.runPromise(\n Stream.runCollect(SpeechSynthesizer.streamSynthesizeDialogue(dialogueReq)).pipe(\n Effect.provide(mock.layer),\n ),\n )\n expect(out.map((c) => Array.from(c.bytes))).toEqual([[7], [8]])\n })\n})\n\ndescribe(\"MultiSpeakerTts capability marker (compile-time)\", () => {\n const dialogueReq: SpeechSynthesizer.CommonSynthesizeDialogueRequest = {\n model: \"mock-tts\",\n turns: [{ voiceId: \"a\", text: \"Hi\" }],\n }\n\n it(\"requires `MultiSpeakerTts` on the R channel of synthesizeDialogue\", () => {\n const eff = SpeechSynthesizer.synthesizeDialogue(dialogueReq)\n expectTypeOf(eff).toEqualTypeOf<\n Effect.Effect<\n AudioBlob,\n AiError.AiError,\n SpeechSynthesizer.SpeechSynthesizer | SpeechSynthesizer.MultiSpeakerTts\n >\n >()\n })\n\n it(\"requires `MultiSpeakerTts` on the R channel of streamSynthesizeDialogue\", () => {\n const s = SpeechSynthesizer.streamSynthesizeDialogue(dialogueReq)\n expectTypeOf(s).toEqualTypeOf<\n Stream.Stream<\n AudioChunk,\n AiError.AiError,\n SpeechSynthesizer.SpeechSynthesizer | SpeechSynthesizer.MultiSpeakerTts\n >\n >()\n })\n\n it(\"a layer without the marker leaves `MultiSpeakerTts` unsatisfied in R\", () => {\n const noMarker = MockSpeechSynthesizer.layerWithoutMultiSpeaker({})\n const program = SpeechSynthesizer.synthesizeDialogue(dialogueReq).pipe(\n Effect.provide(noMarker.layer),\n )\n expectTypeOf(program).toEqualTypeOf<\n Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer.MultiSpeakerTts>\n >()\n })\n\n it(\"a full layer (with marker) clears R to never\", () => {\n const fullMock = MockSpeechSynthesizer.layer({ dialogueBlobs: [blob] })\n const program = SpeechSynthesizer.synthesizeDialogue(dialogueReq).pipe(\n Effect.provide(fullMock.layer),\n )\n expectTypeOf(program).toEqualTypeOf<Effect.Effect<AudioBlob, 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":"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"}
@@ -1,5 +1,5 @@
1
- import { n as AudioChunk } from "../Audio-BfCTGnH3.mjs";
2
- import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, MusicResult } from "../domain/Music.mjs";
1
+ import { n as AudioChunk } from "../Audio-DPlTsNdz.mjs";
2
+ import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, GenerateResult, MusicStreamEvent, audioEvent } from "../domain/Music.mjs";
3
3
  import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
4
4
  import { Effect, Layer } from "effect";
5
5
 
@@ -10,9 +10,14 @@ type MockMusicGeneratorRecorder = {
10
10
  readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>;
11
11
  };
12
12
  type MockMusicGeneratorScript = {
13
- /** One result per `generate` call, consumed in order. */readonly results?: ReadonlyArray<MusicResult>; /** One chunk-list per `streamGeneration` call, consumed in order. */
14
- readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>; /** One chunk-list per `streamGenerationFrom` call, consumed in order. */
15
- readonly streamGenerationFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>;
13
+ /** One result per `generate` call, consumed in order. */readonly results?: ReadonlyArray<GenerateResult>; /** One chunk-list per `streamGeneration` call, consumed in order. */
14
+ readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>;
15
+ /**
16
+ * One event-list per `streamGenerationFrom` call, consumed in order.
17
+ * If a list contains only `AudioChunk`s for ergonomics, wrap them in
18
+ * `audioEvent(chunk)` first.
19
+ */
20
+ readonly streamGenerationFromEvents?: ReadonlyArray<ReadonlyArray<MusicStreamEvent>>;
16
21
  };
17
22
  /**
18
23
  * Layer providing the `MusicGenerator` service AND the
@@ -24,7 +29,7 @@ declare const layer: (script: MockMusicGeneratorScript) => {
24
29
  readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>;
25
30
  };
26
31
  /**
27
- * Variant that omits the `MusicInteractiveSession` marker — simulates a
32
+ * Variant that omits the `MusicInteractiveSession` marker, simulates a
28
33
  * provider without bidirectional support (Lyria 3 sync, ElevenLabs,
29
34
  * Mureka, MiniMax, Stable Audio, Suno). Calls to
30
35
  * `streamGenerationFrom` in code under test should be a compile-time
@@ -35,5 +40,5 @@ declare const layerWithoutInteractive: (script: MockMusicGeneratorScript) => {
35
40
  readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>;
36
41
  };
37
42
  //#endregion
38
- export { MockMusicGeneratorRecorder, MockMusicGeneratorScript, layer, layerWithoutInteractive };
43
+ export { MockMusicGeneratorRecorder, MockMusicGeneratorScript, audioEvent, layer, layerWithoutInteractive };
39
44
  //# sourceMappingURL=MockMusicGenerator.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MockMusicGenerator.d.mts","names":[],"sources":["../../src/testing/MockMusicGenerator.ts"],"mappings":";;;;;;KAeY,0BAAA;EAAA,SACD,aAAA,EAAe,aAAA,CAAc,0BAAA;EAAA,SAC7B,qBAAA,EAAuB,aAAA,CAAc,gCAAA;EAAA,SACrC,yBAAA,EAA2B,aAAA,CAAc,gCAAA;AAAA;AAAA,KAGxC,wBAAA;EALc,kEAOf,OAAA,GAAU,aAAA,CAAc,WAAA,GAND;EAAA,SAQvB,sBAAA,GAAyB,aAAA,CAAc,aAAA,CAAc,UAAA,IAP1B;EAAA,SAS3B,0BAAA,GAA6B,aAAA,CAAc,aAAA,CAAc,UAAA;AAAA;;;;;;cA8EvD,KAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA,GAAiB,uBAAA;EAAA,SACpC,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA;;;AAxFnC;;;;;cAwHa,uBAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA"}
1
+ {"version":3,"file":"MockMusicGenerator.d.mts","names":[],"sources":["../../src/testing/MockMusicGenerator.ts"],"mappings":";;;;;;KAiBY,0BAAA;EAAA,SACD,aAAA,EAAe,aAAA,CAAc,0BAAA;EAAA,SAC7B,qBAAA,EAAuB,aAAA,CAAc,gCAAA;EAAA,SACrC,yBAAA,EAA2B,aAAA,CAAc,gCAAA;AAAA;AAAA,KAGxC,wBAAA;EALc,kEAOf,OAAA,GAAU,aAAA,CAAc,cAAA,GAND;EAAA,SAQvB,sBAAA,GAAyB,aAAA,CAAc,aAAA,CAAc,UAAA;EAP1B;;;;;EAAA,SAa3B,0BAAA,GAA6B,aAAA,CAAc,aAAA,CAAc,gBAAA;AAAA;;;;;;cA+EvD,KAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA,GAAiB,uBAAA;EAAA,SACpC,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA;;;;;;;;cAgCtB,uBAAA,GACX,MAAA,EAAQ,wBAAA;EAAA,SAEC,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,cAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,0BAAA;AAAA"}
@@ -1,4 +1,5 @@
1
1
  import { InvalidRequest } from "../domain/AiError.mjs";
2
+ import { audioEvent } from "../domain/Music.mjs";
2
3
  import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
3
4
  import { Effect, Layer, Ref, Stream } from "effect";
4
5
  //#region src/testing/MockMusicGenerator.ts
@@ -30,7 +31,7 @@ const makeService = (script, record) => Effect.gen(function* () {
30
31
  streamGenerationFrom: (input, request) => Stream.unwrap(Effect.gen(function* () {
31
32
  yield* record.streamGenerationFrom(request);
32
33
  const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1);
33
- const scripted = script.streamGenerationFromChunks ?? [];
34
+ const scripted = script.streamGenerationFromEvents ?? [];
34
35
  if (i >= scripted.length) return Stream.fail(new InvalidRequest({
35
36
  provider: "mock",
36
37
  raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`
@@ -65,7 +66,7 @@ const layer = (script) => {
65
66
  };
66
67
  };
67
68
  /**
68
- * Variant that omits the `MusicInteractiveSession` marker — simulates a
69
+ * Variant that omits the `MusicInteractiveSession` marker, simulates a
69
70
  * provider without bidirectional support (Lyria 3 sync, ElevenLabs,
70
71
  * Mureka, MiniMax, Stable Audio, Suno). Calls to
71
72
  * `streamGenerationFrom` in code under test should be a compile-time
@@ -91,6 +92,6 @@ const layerWithoutInteractive = (script) => {
91
92
  };
92
93
  };
93
94
  //#endregion
94
- export { layer, layerWithoutInteractive };
95
+ export { audioEvent, layer, layerWithoutInteractive };
95
96
 
96
97
  //# sourceMappingURL=MockMusicGenerator.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"MockMusicGenerator.mjs","names":["AiError.InvalidRequest"],"sources":["../../src/testing/MockMusicGenerator.ts"],"sourcesContent":["import { Effect, Layer, Ref, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioChunk } from \"../domain/Audio.js\"\nimport type {\n CommonGenerateMusicRequest,\n CommonStreamGenerateMusicRequest,\n MusicResult,\n MusicSessionInput,\n} from \"../domain/Music.js\"\nimport {\n MusicGenerator,\n MusicInteractiveSession,\n type MusicGeneratorService,\n} from \"../music-generator/MusicGenerator.js\"\n\nexport type MockMusicGeneratorRecorder = {\n readonly generateCalls: ReadonlyArray<CommonGenerateMusicRequest>\n readonly streamGenerationCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n}\n\nexport type MockMusicGeneratorScript = {\n /** One result per `generate` call, consumed in order. */\n readonly results?: ReadonlyArray<MusicResult>\n /** One chunk-list per `streamGeneration` call, consumed in order. */\n readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>\n /** One chunk-list per `streamGenerationFrom` call, consumed in order. */\n readonly streamGenerationFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>\n}\n\nconst makeService = (\n script: MockMusicGeneratorScript,\n record: {\n readonly generate: (req: CommonGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGeneration: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGenerationFrom: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n },\n) =>\n Effect.gen(function* () {\n const gCursor = yield* Ref.make(0)\n const sgCursor = yield* Ref.make(0)\n const sgfCursor = yield* Ref.make(0)\n const service: MusicGeneratorService = {\n generate: (request) =>\n Effect.gen(function* () {\n yield* record.generate(request)\n const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1)\n const scripted = script.results ?? []\n if (i >= scripted.length) {\n return yield* new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`,\n })\n }\n return scripted[i]!\n }),\n streamGeneration: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGeneration(request)\n const i = yield* Ref.getAndUpdate(sgCursor, (n) => n + 1)\n const scripted = script.streamGenerationChunks ?? []\n if (i >= scripted.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGeneration lists scripted, but call ${i + 1} was made`,\n }),\n )\n }\n return Stream.fromIterable(scripted[i]!)\n }),\n ),\n streamGenerationFrom: <E, R>(\n input: Stream.Stream<MusicSessionInput, E, R>,\n request: CommonStreamGenerateMusicRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGenerationFrom(request)\n const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1)\n const scripted = script.streamGenerationFromChunks ?? []\n if (i >= scripted.length) {\n const exhausted: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`,\n }),\n )\n return exhausted\n }\n // Drain the input fully before emitting scripted audio chunks,\n // so consumers can assert on what session messages were pushed.\n return Stream.drain(input).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))\n }),\n ),\n }\n return service\n })\n\n/**\n * Layer providing the `MusicGenerator` service AND the\n * `MusicInteractiveSession` capability marker. Use for the common case\n * where code under test exercises `streamGenerationFrom`.\n */\nexport const layer = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator | MusicInteractiveSession>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const generatorLayer = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n const live = Layer.merge(generatorLayer, Layer.succeed(MusicInteractiveSession, undefined))\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n\n/**\n * Variant that omits the `MusicInteractiveSession` marker — simulates a\n * provider without bidirectional support (Lyria 3 sync, ElevenLabs,\n * Mureka, MiniMax, Stable Audio, Suno). Calls to\n * `streamGenerationFrom` in code under test should be a compile-time\n * error against this Layer alone.\n */\nexport const layerWithoutInteractive = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const live = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MockMusicGenerator.mjs","names":["AiError.InvalidRequest"],"sources":["../../src/testing/MockMusicGenerator.ts"],"sourcesContent":["import { Effect, Layer, Ref, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport type { AudioChunk } from \"../domain/Audio.js\"\nimport {\n audioEvent,\n type CommonGenerateMusicRequest,\n type CommonStreamGenerateMusicRequest,\n type GenerateResult,\n type MusicSessionInput,\n type MusicStreamEvent,\n} from \"../domain/Music.js\"\nimport {\n MusicGenerator,\n MusicInteractiveSession,\n type MusicGeneratorService,\n} from \"../music-generator/MusicGenerator.js\"\n\nexport type MockMusicGeneratorRecorder = {\n readonly generateCalls: ReadonlyArray<CommonGenerateMusicRequest>\n readonly streamGenerationCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>\n}\n\nexport type MockMusicGeneratorScript = {\n /** One result per `generate` call, consumed in order. */\n readonly results?: ReadonlyArray<GenerateResult>\n /** One chunk-list per `streamGeneration` call, consumed in order. */\n readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>\n /**\n * One event-list per `streamGenerationFrom` call, consumed in order.\n * If a list contains only `AudioChunk`s for ergonomics, wrap them in\n * `audioEvent(chunk)` first.\n */\n readonly streamGenerationFromEvents?: ReadonlyArray<ReadonlyArray<MusicStreamEvent>>\n}\n\nconst makeService = (\n script: MockMusicGeneratorScript,\n record: {\n readonly generate: (req: CommonGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGeneration: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n readonly streamGenerationFrom: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>\n },\n) =>\n Effect.gen(function* () {\n const gCursor = yield* Ref.make(0)\n const sgCursor = yield* Ref.make(0)\n const sgfCursor = yield* Ref.make(0)\n const service: MusicGeneratorService = {\n generate: (request) =>\n Effect.gen(function* () {\n yield* record.generate(request)\n const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1)\n const scripted = script.results ?? []\n if (i >= scripted.length) {\n return yield* new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`,\n })\n }\n return scripted[i]!\n }),\n streamGeneration: (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGeneration(request)\n const i = yield* Ref.getAndUpdate(sgCursor, (n) => n + 1)\n const scripted = script.streamGenerationChunks ?? []\n if (i >= scripted.length) {\n return Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGeneration lists scripted, but call ${i + 1} was made`,\n }),\n )\n }\n return Stream.fromIterable(scripted[i]!)\n }),\n ),\n streamGenerationFrom: <E, R>(\n input: Stream.Stream<MusicSessionInput, E, R>,\n request: CommonStreamGenerateMusicRequest,\n ): Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* record.streamGenerationFrom(request)\n const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1)\n const scripted = script.streamGenerationFromEvents ?? []\n if (i >= scripted.length) {\n const exhausted: Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> =\n Stream.fail(\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`,\n }),\n )\n return exhausted\n }\n // Drain the input fully before emitting scripted events,\n // so consumers can assert on what session messages were pushed.\n return Stream.drain(input).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))\n }),\n ),\n }\n return service\n })\n\n/**\n * Layer providing the `MusicGenerator` service AND the\n * `MusicInteractiveSession` capability marker. Use for the common case\n * where code under test exercises `streamGenerationFrom`.\n */\nexport const layer = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator | MusicInteractiveSession>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const generatorLayer = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n const live = Layer.merge(generatorLayer, Layer.succeed(MusicInteractiveSession, undefined))\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n\n/**\n * Variant that omits the `MusicInteractiveSession` marker, simulates a\n * provider without bidirectional support (Lyria 3 sync, ElevenLabs,\n * Mureka, MiniMax, Stable Audio, Suno). Calls to\n * `streamGenerationFrom` in code under test should be a compile-time\n * error against this Layer alone.\n */\nexport const layerWithoutInteractive = (\n script: MockMusicGeneratorScript,\n): {\n readonly layer: Layer.Layer<MusicGenerator>\n readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>\n} => {\n const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])\n const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])\n const live = Layer.effect(\n MusicGenerator,\n makeService(script, {\n generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),\n streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),\n streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),\n }),\n )\n return {\n layer: live,\n recorder: Effect.gen(function* () {\n const generateCalls = yield* Ref.get(gCalls)\n const streamGenerationCalls = yield* Ref.get(sgCalls)\n const streamGenerationFromCalls = yield* Ref.get(sgfCalls)\n return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }\n }),\n }\n}\n\n// Re-export the event constructor for test-script ergonomics.\nexport { audioEvent }\n"],"mappings":";;;;;AAoCA,MAAM,eACJ,QACA,WAMA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,IAAI,KAAK,EAAE;CAClC,MAAM,WAAW,OAAO,IAAI,KAAK,EAAE;CACnC,MAAM,YAAY,OAAO,IAAI,KAAK,EAAE;AAyDpC,QAAO;EAvDL,WAAW,YACT,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,SAAS,QAAQ;GAC/B,MAAM,IAAI,OAAO,IAAI,aAAa,UAAU,MAAM,IAAI,EAAE;GACxD,MAAM,WAAW,OAAO,WAAW,EAAE;AACrC,OAAI,KAAK,SAAS,OAChB,QAAO,OAAO,IAAIA,eAAuB;IACvC,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,8BAA8B,IAAI,EAAE;IAC3F,CAAC;AAEJ,UAAO,SAAS;IAChB;EACJ,mBAAmB,YACjB,OAAO,OACL,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,iBAAiB,QAAQ;GACvC,MAAM,IAAI,OAAO,IAAI,aAAa,WAAW,MAAM,IAAI,EAAE;GACzD,MAAM,WAAW,OAAO,0BAA0B,EAAE;AACpD,OAAI,KAAK,SAAS,OAChB,QAAO,OAAO,KACZ,IAAIA,eAAuB;IACzB,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,6CAA6C,IAAI,EAAE;IAC1G,CAAC,CACH;AAEH,UAAO,OAAO,aAAa,SAAS,GAAI;IACxC,CACH;EACH,uBACE,OACA,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;AACtB,UAAO,OAAO,qBAAqB,QAAQ;GAC3C,MAAM,IAAI,OAAO,IAAI,aAAa,YAAY,MAAM,IAAI,EAAE;GAC1D,MAAM,WAAW,OAAO,8BAA8B,EAAE;AACxD,OAAI,KAAK,SAAS,OAQhB,QANE,OAAO,KACL,IAAIA,eAAuB;IACzB,UAAU;IACV,KAAK,iCAAiC,SAAS,OAAO,iDAAiD,IAAI,EAAE;IAC9G,CAAC,CAEU;AAIlB,UAAO,OAAO,MAAM,MAAM,CAAC,KAAK,OAAO,OAAO,OAAO,aAAa,SAAS,GAAI,CAAC,CAAC;IACjF,CACH;EAES;EACd;;;;;;AAOJ,MAAa,SACX,WAIG;CACH,MAAM,SAAS,IAAI,WAAsD,EAAE,CAAC;CAC5E,MAAM,UAAU,IAAI,WAA4D,EAAE,CAAC;CACnF,MAAM,WAAW,IAAI,WAA4D,EAAE,CAAC;CACpF,MAAM,iBAAiB,MAAM,OAC3B,gBACA,YAAY,QAAQ;EAClB,WAAW,QAAQ,IAAI,OAAO,SAAS,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EAC3D,mBAAmB,QAAQ,IAAI,OAAO,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EACpE,uBAAuB,QAAQ,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;EAC1E,CAAC,CACH;AAED,QAAO;EACL,OAFW,MAAM,MAAM,gBAAgB,MAAM,QAAQ,yBAAyB,KAAA,EAAU,CAE7E;EACX,UAAU,OAAO,IAAI,aAAa;AAIhC,UAAO;IAAE,eAAA,OAHoB,IAAI,IAAI,OAAO;IAGpB,uBAAA,OAFa,IAAI,IAAI,QAAQ;IAEN,2BAAA,OADN,IAAI,IAAI,SAAS;IACgB;IAC1E;EACH;;;;;;;;;AAUH,MAAa,2BACX,WAIG;CACH,MAAM,SAAS,IAAI,WAAsD,EAAE,CAAC;CAC5E,MAAM,UAAU,IAAI,WAA4D,EAAE,CAAC;CACnF,MAAM,WAAW,IAAI,WAA4D,EAAE,CAAC;AASpF,QAAO;EACL,OATW,MAAM,OACjB,gBACA,YAAY,QAAQ;GAClB,WAAW,QAAQ,IAAI,OAAO,SAAS,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GAC3D,mBAAmB,QAAQ,IAAI,OAAO,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GACpE,uBAAuB,QAAQ,IAAI,OAAO,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;GAC1E,CAAC,CAGS;EACX,UAAU,OAAO,IAAI,aAAa;AAIhC,UAAO;IAAE,eAAA,OAHoB,IAAI,IAAI,OAAO;IAGpB,uBAAA,OAFa,IAAI,IAAI,QAAQ;IAEN,2BAAA,OADN,IAAI,IAAI,SAAS;IACgB;IAC1E;EACH"}
@@ -1,5 +1,5 @@
1
- import { d as Item } from "../Items-BH8xUkoR.mjs";
2
- import { r as Turn } from "../Turn-0CwCAyVe.mjs";
1
+ import { o as HistoryItem } from "../Items-D8UaXQx_.mjs";
2
+ import { Turn } from "../domain/Turn.mjs";
3
3
  import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
4
4
  import { Duration, Effect, Layer } from "effect";
5
5
 
@@ -13,7 +13,7 @@ type MockOptions = {
13
13
  readonly deltaInterval?: Duration.Input;
14
14
  };
15
15
  type Call = {
16
- readonly history: ReadonlyArray<Item>;
16
+ readonly history: ReadonlyArray<HistoryItem>;
17
17
  readonly turn: Turn;
18
18
  };
19
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"MockProvider.d.mts","names":[],"sources":["../../src/testing/MockProvider.ts"],"mappings":";;;;;;KAUY,WAAA;;AAAZ;;;;WAMW,aAAA,GAAgB,QAAA,CAAS,KAAA;AAAA;AAAA,KAGxB,IAAA;EAAA,SACD,OAAA,EAAS,aAAA,CAAc,IAAA;EAAA,SACvB,IAAA,EAAM,IAAA;AAAA;;;;;;;KASL,YAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,IAAA;AAAA;;;;;cAsGnB,KAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA,KACT,KAAA,CAAM,KAAA,CAAM,aAAA;AA1Gf;;;;AAAA,cAsHa,iBAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,aAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA;;AApBnC;;;;;cAwCa,IAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,OAAA,EAAS,oBAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA"}
1
+ {"version":3,"file":"MockProvider.d.mts","names":[],"sources":["../../src/testing/MockProvider.ts"],"mappings":";;;;;;KAUY,WAAA;;AAAZ;;;;WAMW,aAAA,GAAgB,QAAA,CAAS,KAAA;AAAA;AAAA,KAGxB,IAAA;EAAA,SACD,OAAA,EAAS,aAAA,CAAc,WAAA;EAAA,SACvB,IAAA,EAAM,IAAA;AAAA;;;;;;;KASL,YAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,IAAA;AAAA;;;;;cAuGnB,KAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA,KACT,KAAA,CAAM,KAAA,CAAM,aAAA;AA3Gf;;;;AAAA,cAuHa,iBAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,aAAA;EAAA,SACnB,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA;;AApBnC;;;;;cAwCa,IAAA,GACX,aAAA,EAAe,aAAA,CAAc,IAAA,GAC7B,OAAA,GAAU,WAAA;EAAA,SAED,OAAA,EAAS,oBAAA;EAAA,SACT,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"MockProvider.mjs","names":["AiError.InvalidRequest","Arr"],"sources":["../../src/testing/MockProvider.ts"],"sourcesContent":["import { Array as Arr, Duration, Effect, Layer, Match, Option, Ref, Schedule, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { type Item, isOutputText } from \"../domain/Items.js\"\nimport {\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"../language-model/LanguageModel.js\"\nimport { type Turn, TurnEvent } from \"../domain/Turn.js\"\n\nexport type MockOptions = {\n /**\n * If set, deltas of each scripted turn are spaced by this duration via\n * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic\n * timing in tests.\n */\n readonly deltaInterval?: Duration.Input\n}\n\nexport type Call = {\n readonly history: ReadonlyArray<Item>\n readonly turn: Turn\n}\n\n/**\n * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,\n * one per call to `streamTurn`. Each scripted turn is split into synthetic\n * deltas (text → ToolCallStart → ToolCallArgsDelta → ... → TurnComplete)\n * so streaming consumers can see realistic delta shapes.\n */\nexport type MockRecorder = {\n readonly calls: ReadonlyArray<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pure projection: Turn → ReadonlyArray<TurnEvent>\n// ---------------------------------------------------------------------------\n\nconst itemToDeltas: (item: Item) => ReadonlyArray<TurnEvent> = Match.type<Item>().pipe(\n Match.discriminators(\"type\")({\n message: (m): ReadonlyArray<TurnEvent> =>\n m.role === \"assistant\"\n ? m.content.filter(isOutputText).map((b) => TurnEvent.TextDelta({ text: b.text }))\n : [],\n function_call: (fc) => [\n TurnEvent.ToolCallStart({ call_id: fc.call_id, name: fc.name }),\n TurnEvent.ToolCallArgsDelta({ call_id: fc.call_id, delta: fc.arguments }),\n ],\n function_call_output: () => [],\n reasoning: (r) =>\n r.summary !== undefined\n ? [TurnEvent.ReasoningDelta({ text: r.summary, kind: \"summary\" as const })]\n : [],\n }),\n Match.exhaustive,\n)\n\nconst turnToDeltas = (turn: Turn): ReadonlyArray<TurnEvent> => [\n ...turn.items.flatMap(itemToDeltas),\n TurnEvent.TurnComplete({ turn }),\n]\n\nconst pacedDeltas = (turn: Turn, options?: MockOptions): Stream.Stream<TurnEvent> => {\n const base = Stream.fromIterable(turnToDeltas(turn))\n return options?.deltaInterval === undefined\n ? base\n : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)))\n}\n\n// ---------------------------------------------------------------------------\n// Canonical service factory. One implementation; sync/Layer/recorder\n// variants below are just different ways to wire the cursor + record hook.\n// ---------------------------------------------------------------------------\n\nconst exhausted = (n: number, attempt: number): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${n} turns scripted, but call ${attempt} was made`,\n })\n\nconst noRecord = (_: Call): Effect.Effect<void> => Effect.void\n\nconst buildService = (\n scriptedTurns: ReadonlyArray<Turn>,\n options: MockOptions | undefined,\n cursor: Ref.Ref<number>,\n record: (call: Call) => Effect.Effect<void>,\n): LanguageModelService => {\n const streamTurn: LanguageModelService[\"streamTurn\"] = (request) =>\n Stream.unwrap(\n Ref.getAndUpdate(cursor, (n) => n + 1).pipe(\n Effect.flatMap(\n (i): Effect.Effect<Stream.Stream<TurnEvent, AiError.AiError>> =>\n Option.match(Arr.get(scriptedTurns, i), {\n onNone: () => Effect.succeed(Stream.fail(exhausted(scriptedTurns.length, i + 1))),\n onSome: (turn) =>\n record({ history: request.history, turn }).pipe(\n Effect.as(pacedDeltas(turn, options)),\n ),\n }),\n ),\n ),\n )\n return { streamTurn, turn: turnFromStream(streamTurn) }\n}\n\n// ---------------------------------------------------------------------------\n// Recorder handle. Unsafe Ref is local: it backs both the `record` write\n// hook (called inside the service) and the `recorder` read effect (called\n// by the test). Both close over the same cell.\n// ---------------------------------------------------------------------------\n\ntype RecorderHandle = {\n readonly record: (call: Call) => Effect.Effect<void>\n readonly recorder: Effect.Effect<MockRecorder>\n}\n\nconst makeRecorderUnsafe = (): RecorderHandle => {\n const ref = Ref.makeUnsafe<ReadonlyArray<Call>>([])\n return {\n record: (call) => Ref.update(ref, Arr.append(call)),\n recorder: Ref.get(ref).pipe(Effect.map((calls): MockRecorder => ({ calls }))),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Layer that registers a `MockProvider` against the `LanguageModel` tag.\n * Calls beyond the scripted turn count fail with `InvalidRequest`.\n */\nexport const layer = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): Layer.Layer<LanguageModel> =>\n Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, noRecord)),\n ),\n )\n\n/**\n * Like `layer`, but also exposes a recorder that captures every call\n * (history + returned turn).\n */\nexport const layerWithRecorder = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly layer: Layer.Layer<LanguageModel>\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const { record, recorder } = makeRecorderUnsafe()\n return {\n layer: Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, record)),\n ),\n ),\n recorder,\n }\n}\n\n/**\n * Build the `LanguageModelService` value directly (no Layer), plus a\n * recorder. Use this when you want to swap models mid-program via\n * `Effect.provideService` instead of providing one model for the whole\n * program via `Layer`.\n */\nexport const make = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly service: LanguageModelService\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const cursor = Ref.makeUnsafe(0)\n const { record, recorder } = makeRecorderUnsafe()\n return {\n service: buildService(scriptedTurns, options, cursor, record),\n recorder,\n }\n}\n"],"mappings":";;;;;;AAsCA,MAAM,eAAyD,MAAM,MAAY,CAAC,KAChF,MAAM,eAAe,OAAO,CAAC;CAC3B,UAAU,MACR,EAAE,SAAS,cACP,EAAE,QAAQ,OAAO,aAAa,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,GAChF,EAAE;CACR,gBAAgB,OAAO,CACrB,UAAU,cAAc;EAAE,SAAS,GAAG;EAAS,MAAM,GAAG;EAAM,CAAC,EAC/D,UAAU,kBAAkB;EAAE,SAAS,GAAG;EAAS,OAAO,GAAG;EAAW,CAAC,CAC1E;CACD,4BAA4B,EAAE;CAC9B,YAAY,MACV,EAAE,YAAY,KAAA,IACV,CAAC,UAAU,eAAe;EAAE,MAAM,EAAE;EAAS,MAAM;EAAoB,CAAC,CAAC,GACzE,EAAE;CACT,CAAC,EACF,MAAM,WACP;AAED,MAAM,gBAAgB,SAAyC,CAC7D,GAAG,KAAK,MAAM,QAAQ,aAAa,EACnC,UAAU,aAAa,EAAE,MAAM,CAAC,CACjC;AAED,MAAM,eAAe,MAAY,YAAoD;CACnF,MAAM,OAAO,OAAO,aAAa,aAAa,KAAK,CAAC;AACpD,QAAO,SAAS,kBAAkB,KAAA,IAC9B,OACA,KAAK,KAAK,OAAO,SAAS,SAAS,OAAO,QAAQ,cAAc,CAAC,CAAC;;AAQxE,MAAM,aAAa,GAAW,YAC5B,IAAIA,eAAuB;CACzB,UAAU;CACV,KAAK,2BAA2B,EAAE,4BAA4B,QAAQ;CACvE,CAAC;AAEJ,MAAM,YAAY,MAAiC,OAAO;AAE1D,MAAM,gBACJ,eACA,SACA,QACA,WACyB;CACzB,MAAM,cAAkD,YACtD,OAAO,OACL,IAAI,aAAa,SAAS,MAAM,IAAI,EAAE,CAAC,KACrC,OAAO,SACJ,MACC,OAAO,MAAMC,MAAI,IAAI,eAAe,EAAE,EAAE;EACtC,cAAc,OAAO,QAAQ,OAAO,KAAK,UAAU,cAAc,QAAQ,IAAI,EAAE,CAAC,CAAC;EACjF,SAAS,SACP,OAAO;GAAE,SAAS,QAAQ;GAAS;GAAM,CAAC,CAAC,KACzC,OAAO,GAAG,YAAY,MAAM,QAAQ,CAAC,CACtC;EACJ,CAAC,CACL,CACF,CACF;AACH,QAAO;EAAE;EAAY,MAAM,eAAe,WAAW;EAAE;;AAczD,MAAM,2BAA2C;CAC/C,MAAM,MAAM,IAAI,WAAgC,EAAE,CAAC;AACnD,QAAO;EACL,SAAS,SAAS,IAAI,OAAO,KAAKA,MAAI,OAAO,KAAK,CAAC;EACnD,UAAU,IAAI,IAAI,IAAI,CAAC,KAAK,OAAO,KAAK,WAAyB,EAAE,OAAO,EAAE,CAAC;EAC9E;;;;;;AAWH,MAAa,SACX,eACA,YAEA,MAAM,OACJ,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,SAAS,CAAC,CAC/E,CACF;;;;;AAMH,MAAa,qBACX,eACA,YAIG;CACH,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,OAAO,MAAM,OACX,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,OAAO,CAAC,CAC7E,CACF;EACD;EACD;;;;;;;;AASH,MAAa,QACX,eACA,YAIG;CACH,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,SAAS,aAAa,eAAe,SAAS,QAAQ,OAAO;EAC7D;EACD"}
1
+ {"version":3,"file":"MockProvider.mjs","names":["AiError.InvalidRequest","Arr"],"sources":["../../src/testing/MockProvider.ts"],"sourcesContent":["import { Array as Arr, Duration, Effect, Layer, Match, Option, Ref, Schedule, Stream } from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { type HistoryItem, isOutputText } from \"../domain/Items.js\"\nimport {\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"../language-model/LanguageModel.js\"\nimport { type Turn, TurnEvent } from \"../domain/Turn.js\"\n\nexport type MockOptions = {\n /**\n * If set, deltas of each scripted turn are spaced by this duration via\n * `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic\n * timing in tests.\n */\n readonly deltaInterval?: Duration.Input\n}\n\nexport type Call = {\n readonly history: ReadonlyArray<HistoryItem>\n readonly turn: Turn\n}\n\n/**\n * A scripted mock provider. Pre-canned `Turn` outputs are returned in order,\n * one per call to `streamTurn`. Each scripted turn is split into synthetic\n * deltas (text → ToolCallStart → ToolCallArgsDelta → ... → TurnComplete)\n * so streaming consumers can see realistic delta shapes.\n */\nexport type MockRecorder = {\n readonly calls: ReadonlyArray<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pure projection: Turn → ReadonlyArray<TurnEvent>\n// ---------------------------------------------------------------------------\n\nconst itemToDeltas: (item: HistoryItem) => ReadonlyArray<TurnEvent> =\n Match.type<HistoryItem>().pipe(\n Match.discriminators(\"type\")({\n message: (m): ReadonlyArray<TurnEvent> =>\n m.role === \"assistant\"\n ? m.content.filter(isOutputText).map((b) => TurnEvent.TextDelta({ text: b.text }))\n : [],\n function_call: (fc) => [\n TurnEvent.ToolCallStart({ call_id: fc.call_id, name: fc.name }),\n TurnEvent.ToolCallArgsDelta({ call_id: fc.call_id, delta: fc.arguments }),\n ],\n function_call_output: () => [],\n reasoning: (r) =>\n r.summary !== undefined\n ? [TurnEvent.ReasoningDelta({ text: r.summary, kind: \"summary\" as const })]\n : [],\n }),\n Match.exhaustive,\n )\n\nconst turnToDeltas = (turn: Turn): ReadonlyArray<TurnEvent> => [\n ...turn.items.flatMap(itemToDeltas),\n TurnEvent.TurnComplete({ turn }),\n]\n\nconst pacedDeltas = (turn: Turn, options?: MockOptions): Stream.Stream<TurnEvent> => {\n const base = Stream.fromIterable(turnToDeltas(turn))\n return options?.deltaInterval === undefined\n ? base\n : base.pipe(Stream.schedule(Schedule.spaced(options.deltaInterval)))\n}\n\n// ---------------------------------------------------------------------------\n// Canonical service factory. One implementation; sync/Layer/recorder\n// variants below are just different ways to wire the cursor + record hook.\n// ---------------------------------------------------------------------------\n\nconst exhausted = (n: number, attempt: number): AiError.AiError =>\n new AiError.InvalidRequest({\n provider: \"mock\",\n raw: `MockProvider exhausted: ${n} turns scripted, but call ${attempt} was made`,\n })\n\nconst noRecord = (_: Call): Effect.Effect<void> => Effect.void\n\nconst buildService = (\n scriptedTurns: ReadonlyArray<Turn>,\n options: MockOptions | undefined,\n cursor: Ref.Ref<number>,\n record: (call: Call) => Effect.Effect<void>,\n): LanguageModelService => {\n const streamTurn: LanguageModelService[\"streamTurn\"] = (request) =>\n Stream.unwrap(\n Ref.getAndUpdate(cursor, (n) => n + 1).pipe(\n Effect.flatMap(\n (i): Effect.Effect<Stream.Stream<TurnEvent, AiError.AiError>> =>\n Option.match(Arr.get(scriptedTurns, i), {\n onNone: () => Effect.succeed(Stream.fail(exhausted(scriptedTurns.length, i + 1))),\n onSome: (turn) =>\n record({ history: request.history, turn }).pipe(\n Effect.as(pacedDeltas(turn, options)),\n ),\n }),\n ),\n ),\n )\n return { streamTurn, turn: turnFromStream(streamTurn) }\n}\n\n// ---------------------------------------------------------------------------\n// Recorder handle. Unsafe Ref is local: it backs both the `record` write\n// hook (called inside the service) and the `recorder` read effect (called\n// by the test). Both close over the same cell.\n// ---------------------------------------------------------------------------\n\ntype RecorderHandle = {\n readonly record: (call: Call) => Effect.Effect<void>\n readonly recorder: Effect.Effect<MockRecorder>\n}\n\nconst makeRecorderUnsafe = (): RecorderHandle => {\n const ref = Ref.makeUnsafe<ReadonlyArray<Call>>([])\n return {\n record: (call) => Ref.update(ref, Arr.append(call)),\n recorder: Ref.get(ref).pipe(Effect.map((calls): MockRecorder => ({ calls }))),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Layer that registers a `MockProvider` against the `LanguageModel` tag.\n * Calls beyond the scripted turn count fail with `InvalidRequest`.\n */\nexport const layer = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): Layer.Layer<LanguageModel> =>\n Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, noRecord)),\n ),\n )\n\n/**\n * Like `layer`, but also exposes a recorder that captures every call\n * (history + returned turn).\n */\nexport const layerWithRecorder = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly layer: Layer.Layer<LanguageModel>\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const { record, recorder } = makeRecorderUnsafe()\n return {\n layer: Layer.effect(\n LanguageModel,\n Ref.make(0).pipe(\n Effect.map((cursor) => buildService(scriptedTurns, options, cursor, record)),\n ),\n ),\n recorder,\n }\n}\n\n/**\n * Build the `LanguageModelService` value directly (no Layer), plus a\n * recorder. Use this when you want to swap models mid-program via\n * `Effect.provideService` instead of providing one model for the whole\n * program via `Layer`.\n */\nexport const make = (\n scriptedTurns: ReadonlyArray<Turn>,\n options?: MockOptions,\n): {\n readonly service: LanguageModelService\n readonly recorder: Effect.Effect<MockRecorder>\n} => {\n const cursor = Ref.makeUnsafe(0)\n const { record, recorder } = makeRecorderUnsafe()\n return {\n service: buildService(scriptedTurns, options, cursor, record),\n recorder,\n }\n}\n"],"mappings":";;;;;;AAsCA,MAAM,eACJ,MAAM,MAAmB,CAAC,KACxB,MAAM,eAAe,OAAO,CAAC;CAC3B,UAAU,MACR,EAAE,SAAS,cACP,EAAE,QAAQ,OAAO,aAAa,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,GAChF,EAAE;CACR,gBAAgB,OAAO,CACrB,UAAU,cAAc;EAAE,SAAS,GAAG;EAAS,MAAM,GAAG;EAAM,CAAC,EAC/D,UAAU,kBAAkB;EAAE,SAAS,GAAG;EAAS,OAAO,GAAG;EAAW,CAAC,CAC1E;CACD,4BAA4B,EAAE;CAC9B,YAAY,MACV,EAAE,YAAY,KAAA,IACV,CAAC,UAAU,eAAe;EAAE,MAAM,EAAE;EAAS,MAAM;EAAoB,CAAC,CAAC,GACzE,EAAE;CACT,CAAC,EACF,MAAM,WACP;AAEH,MAAM,gBAAgB,SAAyC,CAC7D,GAAG,KAAK,MAAM,QAAQ,aAAa,EACnC,UAAU,aAAa,EAAE,MAAM,CAAC,CACjC;AAED,MAAM,eAAe,MAAY,YAAoD;CACnF,MAAM,OAAO,OAAO,aAAa,aAAa,KAAK,CAAC;AACpD,QAAO,SAAS,kBAAkB,KAAA,IAC9B,OACA,KAAK,KAAK,OAAO,SAAS,SAAS,OAAO,QAAQ,cAAc,CAAC,CAAC;;AAQxE,MAAM,aAAa,GAAW,YAC5B,IAAIA,eAAuB;CACzB,UAAU;CACV,KAAK,2BAA2B,EAAE,4BAA4B,QAAQ;CACvE,CAAC;AAEJ,MAAM,YAAY,MAAiC,OAAO;AAE1D,MAAM,gBACJ,eACA,SACA,QACA,WACyB;CACzB,MAAM,cAAkD,YACtD,OAAO,OACL,IAAI,aAAa,SAAS,MAAM,IAAI,EAAE,CAAC,KACrC,OAAO,SACJ,MACC,OAAO,MAAMC,MAAI,IAAI,eAAe,EAAE,EAAE;EACtC,cAAc,OAAO,QAAQ,OAAO,KAAK,UAAU,cAAc,QAAQ,IAAI,EAAE,CAAC,CAAC;EACjF,SAAS,SACP,OAAO;GAAE,SAAS,QAAQ;GAAS;GAAM,CAAC,CAAC,KACzC,OAAO,GAAG,YAAY,MAAM,QAAQ,CAAC,CACtC;EACJ,CAAC,CACL,CACF,CACF;AACH,QAAO;EAAE;EAAY,MAAM,eAAe,WAAW;EAAE;;AAczD,MAAM,2BAA2C;CAC/C,MAAM,MAAM,IAAI,WAAgC,EAAE,CAAC;AACnD,QAAO;EACL,SAAS,SAAS,IAAI,OAAO,KAAKA,MAAI,OAAO,KAAK,CAAC;EACnD,UAAU,IAAI,IAAI,IAAI,CAAC,KAAK,OAAO,KAAK,WAAyB,EAAE,OAAO,EAAE,CAAC;EAC9E;;;;;;AAWH,MAAa,SACX,eACA,YAEA,MAAM,OACJ,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,SAAS,CAAC,CAC/E,CACF;;;;;AAMH,MAAa,qBACX,eACA,YAIG;CACH,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,OAAO,MAAM,OACX,eACA,IAAI,KAAK,EAAE,CAAC,KACV,OAAO,KAAK,WAAW,aAAa,eAAe,SAAS,QAAQ,OAAO,CAAC,CAC7E,CACF;EACD;EACD;;;;;;;;AASH,MAAa,QACX,eACA,YAIG;CACH,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,EAAE,QAAQ,aAAa,oBAAoB;AACjD,QAAO;EACL,SAAS,aAAa,eAAe,SAAS,QAAQ,OAAO;EAC7D;EACD"}