@effect-uai/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
- package/dist/Audio-BfCTGnH3.d.mts +61 -0
- package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
- package/dist/Image-DxyXqzAM.d.mts +61 -0
- package/dist/Image-DxyXqzAM.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
- package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
- package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
- package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
- package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
- package/dist/Turn-7geUcKsf.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +19 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +2 -0
- package/dist/domain/Audio.mjs +14 -0
- package/dist/domain/Audio.mjs.map +1 -0
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Music.d.mts +116 -0
- package/dist/domain/Music.d.mts.map +1 -0
- package/dist/domain/Music.mjs +29 -0
- package/dist/domain/Music.mjs.map +1 -0
- package/dist/domain/Transcript.d.mts +95 -0
- package/dist/domain/Transcript.d.mts.map +1 -0
- package/dist/domain/Transcript.mjs +22 -0
- package/dist/domain/Transcript.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +21 -7
- package/dist/index.mjs +16 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +31 -7
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.d.mts +77 -0
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
- package/dist/music-generator/MusicGenerator.mjs +51 -0
- package/dist/music-generator/MusicGenerator.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
- package/dist/music-generator/MusicGenerator.test.mjs +154 -0
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
- package/dist/streaming/JSONL.d.mts +10 -3
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +13 -2
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +39 -0
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
- package/dist/testing/MockMusicGenerator.mjs +96 -0
- package/dist/testing/MockMusicGenerator.mjs.map +1 -0
- package/dist/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
- package/dist/testing/MockTranscriber.d.mts +37 -0
- package/dist/testing/MockTranscriber.d.mts.map +1 -0
- package/dist/testing/MockTranscriber.mjs +77 -0
- package/dist/testing/MockTranscriber.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +32 -10
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +11 -8
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +10 -1
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +19 -10
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +5 -5
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/dist/transcriber/Transcriber.d.mts +101 -0
- package/dist/transcriber/Transcriber.d.mts.map +1 -0
- package/dist/transcriber/Transcriber.mjs +49 -0
- package/dist/transcriber/Transcriber.mjs.map +1 -0
- package/dist/transcriber/Transcriber.test.d.mts +1 -0
- package/dist/transcriber/Transcriber.test.mjs +130 -0
- package/dist/transcriber/Transcriber.test.mjs.map +1 -0
- package/package.json +65 -13
- package/src/domain/AiError.ts +21 -0
- package/src/domain/Audio.ts +88 -0
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Music.ts +121 -0
- package/src/domain/Transcript.ts +83 -0
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +15 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/music-generator/MusicGenerator.test.ts +170 -0
- package/src/music-generator/MusicGenerator.ts +123 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
- package/src/streaming/JSONL.ts +12 -0
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockMusicGenerator.ts +170 -0
- package/src/testing/MockProvider.ts +2 -2
- package/src/testing/MockSpeechSynthesizer.ts +165 -0
- package/src/testing/MockTranscriber.ts +139 -0
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +36 -36
- package/src/tool/Resolvers.test.ts +11 -35
- package/src/tool/Resolvers.ts +5 -14
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +28 -24
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +57 -33
- package/src/transcriber/Transcriber.test.ts +125 -0
- package/src/transcriber/Transcriber.ts +127 -0
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Outcome-GiaNvt7i.d.mts +0 -32
- package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
- package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
- package/dist/Turn-Bi83du4I.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- package/src/match/Match.ts +0 -9
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Context, Effect, Function, Stream } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
import type { AudioBlob, AudioChunk, AudioFormat } from "../domain/Audio.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cross-provider synthesis request. Provider-specific extensions
|
|
7
|
+
* (ElevenLabs `stability` / `similarity_boost`, Cartesia `emotion`,
|
|
8
|
+
* MiniMax `vol` / `pitch`, Azure SSML style tags) live on each
|
|
9
|
+
* provider's typed request which extends this and narrows `model` and
|
|
10
|
+
* `voiceId`.
|
|
11
|
+
*/
|
|
12
|
+
export type CommonSynthesizeRequest = {
|
|
13
|
+
readonly text: string
|
|
14
|
+
/** Model identifier. Each provider narrows. */
|
|
15
|
+
readonly model: string
|
|
16
|
+
/**
|
|
17
|
+
* Voice identifier. Per-provider request types narrow this to a
|
|
18
|
+
* typed literal union of stock voices + `(string & {})` escape for
|
|
19
|
+
* custom cloned voice IDs. Providers without custom-voice support
|
|
20
|
+
* (OpenAI, Deepgram Aura, AWS Polly) narrow to the stock-only union.
|
|
21
|
+
*/
|
|
22
|
+
readonly voiceId: string
|
|
23
|
+
readonly outputFormat?: AudioFormat
|
|
24
|
+
readonly speed?: number
|
|
25
|
+
readonly languageCode?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Incremental-synthesis request — text arrives as `Stream<string>`.
|
|
30
|
+
* Gated by the `TtsIncrementalText` capability marker; only providers
|
|
31
|
+
* that ship the marker can be used.
|
|
32
|
+
*
|
|
33
|
+
* Multi-context features (Cartesia `context_id`, ElevenLabs `multi-
|
|
34
|
+
* stream-input`) are NOT exposed here — one logical utterance per
|
|
35
|
+
* call. Provider extensions can expose `forkContext` for that.
|
|
36
|
+
*/
|
|
37
|
+
export type CommonStreamSynthesizeRequest = Omit<CommonSynthesizeRequest, "text">
|
|
38
|
+
|
|
39
|
+
export type SpeechSynthesizerService = {
|
|
40
|
+
/** One-shot. Full text in, full audio bytes out. Universally supported. */
|
|
41
|
+
readonly synthesize: (
|
|
42
|
+
request: CommonSynthesizeRequest,
|
|
43
|
+
) => Effect.Effect<AudioBlob, AiError.AiError>
|
|
44
|
+
/**
|
|
45
|
+
* Full text in, audio chunks streamed out (chunked HTTP). Universally
|
|
46
|
+
* supported across providers that offer any streaming TTS at all.
|
|
47
|
+
*/
|
|
48
|
+
readonly streamSynthesis: (
|
|
49
|
+
request: CommonSynthesizeRequest,
|
|
50
|
+
) => Stream.Stream<AudioChunk, AiError.AiError>
|
|
51
|
+
/**
|
|
52
|
+
* Incremental text in (as a Stream), audio chunks streamed out. The
|
|
53
|
+
* underlying WS connection is acquired on first pull and released
|
|
54
|
+
* when the output stream is finalized via `Stream.scoped`.
|
|
55
|
+
*
|
|
56
|
+
* Gated by the `TtsIncrementalText` capability marker on the top-
|
|
57
|
+
* level helper — providers without WS-style incremental input don't
|
|
58
|
+
* ship the marker, so calls fail at `Effect.provide` with a type
|
|
59
|
+
* error.
|
|
60
|
+
*/
|
|
61
|
+
readonly streamSynthesisFrom: <E, R>(
|
|
62
|
+
textIn: Stream.Stream<string, E, R>,
|
|
63
|
+
request: CommonStreamSynthesizeRequest,
|
|
64
|
+
) => Stream.Stream<AudioChunk, AiError.AiError | E, R>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class SpeechSynthesizer extends Context.Service<
|
|
68
|
+
SpeechSynthesizer,
|
|
69
|
+
SpeechSynthesizerService
|
|
70
|
+
>()("@betalyra/effect-uai/SpeechSynthesizer") {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Capability marker — provided by provider layers whose
|
|
74
|
+
* `streamSynthesisFrom` is wired up at the wire level. OpenAI, Azure
|
|
75
|
+
* (wire), and AWS Polly non-Generative do not ship it. Calling
|
|
76
|
+
* `streamSynthesisFrom` while only one of those Layers is in scope
|
|
77
|
+
* fails at `Effect.provide` with a type error.
|
|
78
|
+
*
|
|
79
|
+
* Phantom — the value is `void`; providers register with
|
|
80
|
+
* `Layer.succeed(TtsIncrementalText, undefined)`.
|
|
81
|
+
*/
|
|
82
|
+
export class TtsIncrementalText extends Context.Service<TtsIncrementalText, void>()(
|
|
83
|
+
"@betalyra/effect-uai/capability/TtsIncrementalText",
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
/** One-shot synthesis. */
|
|
87
|
+
export const synthesize = (
|
|
88
|
+
request: CommonSynthesizeRequest,
|
|
89
|
+
): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer> =>
|
|
90
|
+
Effect.flatMap(SpeechSynthesizer.asEffect(), (s) => s.synthesize(request))
|
|
91
|
+
|
|
92
|
+
/** Full text in, audio chunks out. */
|
|
93
|
+
export const streamSynthesis = (
|
|
94
|
+
request: CommonSynthesizeRequest,
|
|
95
|
+
): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer> =>
|
|
96
|
+
Stream.unwrap(Effect.map(SpeechSynthesizer.asEffect(), (s) => s.streamSynthesis(request)))
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Incremental synthesis. Dual-arity: pipeable (data-last) and direct
|
|
100
|
+
* (data-first). Requires `TtsIncrementalText` in R — providers without
|
|
101
|
+
* incremental-text-in support are a type error at provide time.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const audio = LanguageModel.streamTurn(turnReq).pipe(
|
|
106
|
+
* Stream.filterMap(Turn.toTextDelta),
|
|
107
|
+
* SpeechSynthesizer.streamSynthesisFrom(synthReq),
|
|
108
|
+
* )
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const streamSynthesisFrom: {
|
|
112
|
+
(
|
|
113
|
+
request: CommonStreamSynthesizeRequest,
|
|
114
|
+
): <E, R>(
|
|
115
|
+
textIn: Stream.Stream<string, E, R>,
|
|
116
|
+
) => Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>
|
|
117
|
+
<E, R>(
|
|
118
|
+
textIn: Stream.Stream<string, E, R>,
|
|
119
|
+
request: CommonStreamSynthesizeRequest,
|
|
120
|
+
): Stream.Stream<AudioChunk, AiError.AiError | E, R | SpeechSynthesizer | TtsIncrementalText>
|
|
121
|
+
} = Function.dual(
|
|
122
|
+
2,
|
|
123
|
+
<E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) =>
|
|
124
|
+
Stream.unwrap(
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const s = yield* SpeechSynthesizer.asEffect()
|
|
127
|
+
yield* TtsIncrementalText.asEffect()
|
|
128
|
+
return s.streamSynthesisFrom(textIn, request)
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
)
|
package/src/streaming/JSONL.ts
CHANGED
|
@@ -79,6 +79,18 @@ export const parse =
|
|
|
79
79
|
),
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Best-effort parse of a single JSON frame. Returns the parsed value or
|
|
84
|
+
* `undefined` on malformed input. Realtime WS adapters use this to skip
|
|
85
|
+
* non-JSON or partially-received frames silently rather than fail the
|
|
86
|
+
* entire session over one bad frame.
|
|
87
|
+
*/
|
|
88
|
+
export const parseSafe = (raw: string) =>
|
|
89
|
+
Effect.try({
|
|
90
|
+
try: () => JSON.parse(raw) as unknown,
|
|
91
|
+
catch: () => undefined,
|
|
92
|
+
}).pipe(Effect.orElseSucceed(() => undefined))
|
|
93
|
+
|
|
82
94
|
const encoder = new TextEncoder()
|
|
83
95
|
|
|
84
96
|
/**
|
package/src/streaming/SSE.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type StructuredSchema<Output = unknown> = StandardSchemaV1<unknown, Outpu
|
|
|
19
19
|
* cross-validator schema with metadata providers need (name, description,
|
|
20
20
|
* strict-mode flag).
|
|
21
21
|
*/
|
|
22
|
-
export
|
|
22
|
+
export type StructuredFormat<A> = {
|
|
23
23
|
readonly name: string
|
|
24
24
|
readonly description?: string
|
|
25
25
|
readonly schema: StructuredSchema<A>
|
|
@@ -31,7 +31,7 @@ export interface StructuredFormat<A> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** A single path-scoped validation problem. Library-agnostic shape. */
|
|
34
|
-
export
|
|
34
|
+
export type DecodeIssue = {
|
|
35
35
|
readonly path: ReadonlyArray<string | number>
|
|
36
36
|
readonly message: string
|
|
37
37
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Effect, Layer, Ref, Stream } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
import type { AudioChunk } from "../domain/Audio.js"
|
|
4
|
+
import type {
|
|
5
|
+
CommonGenerateMusicRequest,
|
|
6
|
+
CommonStreamGenerateMusicRequest,
|
|
7
|
+
MusicResult,
|
|
8
|
+
MusicSessionInput,
|
|
9
|
+
} from "../domain/Music.js"
|
|
10
|
+
import {
|
|
11
|
+
MusicGenerator,
|
|
12
|
+
MusicInteractiveSession,
|
|
13
|
+
type MusicGeneratorService,
|
|
14
|
+
} from "../music-generator/MusicGenerator.js"
|
|
15
|
+
|
|
16
|
+
export type MockMusicGeneratorRecorder = {
|
|
17
|
+
readonly generateCalls: ReadonlyArray<CommonGenerateMusicRequest>
|
|
18
|
+
readonly streamGenerationCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>
|
|
19
|
+
readonly streamGenerationFromCalls: ReadonlyArray<CommonStreamGenerateMusicRequest>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MockMusicGeneratorScript = {
|
|
23
|
+
/** One result per `generate` call, consumed in order. */
|
|
24
|
+
readonly results?: ReadonlyArray<MusicResult>
|
|
25
|
+
/** One chunk-list per `streamGeneration` call, consumed in order. */
|
|
26
|
+
readonly streamGenerationChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
27
|
+
/** One chunk-list per `streamGenerationFrom` call, consumed in order. */
|
|
28
|
+
readonly streamGenerationFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const makeService = (
|
|
32
|
+
script: MockMusicGeneratorScript,
|
|
33
|
+
record: {
|
|
34
|
+
readonly generate: (req: CommonGenerateMusicRequest) => Effect.Effect<void>
|
|
35
|
+
readonly streamGeneration: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>
|
|
36
|
+
readonly streamGenerationFrom: (req: CommonStreamGenerateMusicRequest) => Effect.Effect<void>
|
|
37
|
+
},
|
|
38
|
+
) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const gCursor = yield* Ref.make(0)
|
|
41
|
+
const sgCursor = yield* Ref.make(0)
|
|
42
|
+
const sgfCursor = yield* Ref.make(0)
|
|
43
|
+
const service: MusicGeneratorService = {
|
|
44
|
+
generate: (request) =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
yield* record.generate(request)
|
|
47
|
+
const i = yield* Ref.getAndUpdate(gCursor, (n) => n + 1)
|
|
48
|
+
const scripted = script.results ?? []
|
|
49
|
+
if (i >= scripted.length) {
|
|
50
|
+
return yield* Effect.fail(
|
|
51
|
+
new AiError.InvalidRequest({
|
|
52
|
+
provider: "mock",
|
|
53
|
+
raw: `MockMusicGenerator exhausted: ${scripted.length} results scripted, but call ${i + 1} was made`,
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
return scripted[i]!
|
|
58
|
+
}),
|
|
59
|
+
streamGeneration: (request) =>
|
|
60
|
+
Stream.unwrap(
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
yield* record.streamGeneration(request)
|
|
63
|
+
const i = yield* Ref.getAndUpdate(sgCursor, (n) => n + 1)
|
|
64
|
+
const scripted = script.streamGenerationChunks ?? []
|
|
65
|
+
if (i >= scripted.length) {
|
|
66
|
+
return Stream.fail(
|
|
67
|
+
new AiError.InvalidRequest({
|
|
68
|
+
provider: "mock",
|
|
69
|
+
raw: `MockMusicGenerator exhausted: ${scripted.length} streamGeneration lists scripted, but call ${i + 1} was made`,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return Stream.fromIterable(scripted[i]!)
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
streamGenerationFrom: <E, R>(
|
|
77
|
+
input: Stream.Stream<MusicSessionInput, E, R>,
|
|
78
|
+
request: CommonStreamGenerateMusicRequest,
|
|
79
|
+
): Stream.Stream<AudioChunk, AiError.AiError | E, R> =>
|
|
80
|
+
Stream.unwrap(
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
yield* record.streamGenerationFrom(request)
|
|
83
|
+
const i = yield* Ref.getAndUpdate(sgfCursor, (n) => n + 1)
|
|
84
|
+
const scripted = script.streamGenerationFromChunks ?? []
|
|
85
|
+
if (i >= scripted.length) {
|
|
86
|
+
const exhausted: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(
|
|
87
|
+
new AiError.InvalidRequest({
|
|
88
|
+
provider: "mock",
|
|
89
|
+
raw: `MockMusicGenerator exhausted: ${scripted.length} streamGenerationFrom lists scripted, but call ${i + 1} was made`,
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
return exhausted
|
|
93
|
+
}
|
|
94
|
+
// Drain the input fully before emitting scripted audio chunks,
|
|
95
|
+
// so consumers can assert on what session messages were pushed.
|
|
96
|
+
return Stream.drain(input).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
return service
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Layer providing the `MusicGenerator` service AND the
|
|
105
|
+
* `MusicInteractiveSession` capability marker. Use for the common case
|
|
106
|
+
* where code under test exercises `streamGenerationFrom`.
|
|
107
|
+
*/
|
|
108
|
+
export const layer = (
|
|
109
|
+
script: MockMusicGeneratorScript,
|
|
110
|
+
): {
|
|
111
|
+
readonly layer: Layer.Layer<MusicGenerator | MusicInteractiveSession>
|
|
112
|
+
readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>
|
|
113
|
+
} => {
|
|
114
|
+
const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])
|
|
115
|
+
const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])
|
|
116
|
+
const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])
|
|
117
|
+
const generatorLayer = Layer.effect(
|
|
118
|
+
MusicGenerator,
|
|
119
|
+
makeService(script, {
|
|
120
|
+
generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),
|
|
121
|
+
streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),
|
|
122
|
+
streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
const live = Layer.merge(generatorLayer, Layer.succeed(MusicInteractiveSession, undefined))
|
|
126
|
+
return {
|
|
127
|
+
layer: live,
|
|
128
|
+
recorder: Effect.gen(function* () {
|
|
129
|
+
const generateCalls = yield* Ref.get(gCalls)
|
|
130
|
+
const streamGenerationCalls = yield* Ref.get(sgCalls)
|
|
131
|
+
const streamGenerationFromCalls = yield* Ref.get(sgfCalls)
|
|
132
|
+
return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Variant that omits the `MusicInteractiveSession` marker — simulates a
|
|
139
|
+
* provider without bidirectional support (Lyria 3 sync, ElevenLabs,
|
|
140
|
+
* Mureka, MiniMax, Stable Audio, Suno). Calls to
|
|
141
|
+
* `streamGenerationFrom` in code under test should be a compile-time
|
|
142
|
+
* error against this Layer alone.
|
|
143
|
+
*/
|
|
144
|
+
export const layerWithoutInteractive = (
|
|
145
|
+
script: MockMusicGeneratorScript,
|
|
146
|
+
): {
|
|
147
|
+
readonly layer: Layer.Layer<MusicGenerator>
|
|
148
|
+
readonly recorder: Effect.Effect<MockMusicGeneratorRecorder>
|
|
149
|
+
} => {
|
|
150
|
+
const gCalls = Ref.makeUnsafe<ReadonlyArray<CommonGenerateMusicRequest>>([])
|
|
151
|
+
const sgCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])
|
|
152
|
+
const sgfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamGenerateMusicRequest>>([])
|
|
153
|
+
const live = Layer.effect(
|
|
154
|
+
MusicGenerator,
|
|
155
|
+
makeService(script, {
|
|
156
|
+
generate: (req) => Ref.update(gCalls, (xs) => [...xs, req]),
|
|
157
|
+
streamGeneration: (req) => Ref.update(sgCalls, (xs) => [...xs, req]),
|
|
158
|
+
streamGenerationFrom: (req) => Ref.update(sgfCalls, (xs) => [...xs, req]),
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
return {
|
|
162
|
+
layer: live,
|
|
163
|
+
recorder: Effect.gen(function* () {
|
|
164
|
+
const generateCalls = yield* Ref.get(gCalls)
|
|
165
|
+
const streamGenerationCalls = yield* Ref.get(sgCalls)
|
|
166
|
+
const streamGenerationFromCalls = yield* Ref.get(sgfCalls)
|
|
167
|
+
return { generateCalls, streamGenerationCalls, streamGenerationFromCalls }
|
|
168
|
+
}),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { Item } from "../domain/Items.js"
|
|
|
4
4
|
import { LanguageModel, type LanguageModelService } from "../language-model/LanguageModel.js"
|
|
5
5
|
import type { Turn, TurnEvent } from "../domain/Turn.js"
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type MockOptions = {
|
|
8
8
|
/**
|
|
9
9
|
* If set, deltas of each scripted turn are spaced by this duration via
|
|
10
10
|
* `Schedule.spaced`. Combine with `TestClock.adjust` for deterministic
|
|
@@ -19,7 +19,7 @@ export interface MockOptions {
|
|
|
19
19
|
* deltas (text → tool_call_start → tool_call_args_delta → ... → turn_complete)
|
|
20
20
|
* so streaming consumers can see realistic delta shapes.
|
|
21
21
|
*/
|
|
22
|
-
export
|
|
22
|
+
export type MockRecorder = {
|
|
23
23
|
readonly calls: ReadonlyArray<{
|
|
24
24
|
readonly history: ReadonlyArray<Item>
|
|
25
25
|
readonly turn: Turn
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Effect, Layer, Ref, Stream } from "effect"
|
|
2
|
+
import type { AudioBlob, AudioChunk } from "../domain/Audio.js"
|
|
3
|
+
import * as AiError from "../domain/AiError.js"
|
|
4
|
+
import {
|
|
5
|
+
SpeechSynthesizer,
|
|
6
|
+
TtsIncrementalText,
|
|
7
|
+
type CommonStreamSynthesizeRequest,
|
|
8
|
+
type CommonSynthesizeRequest,
|
|
9
|
+
type SpeechSynthesizerService,
|
|
10
|
+
} from "../speech-synthesizer/SpeechSynthesizer.js"
|
|
11
|
+
|
|
12
|
+
export type MockSynthesizerRecorder = {
|
|
13
|
+
readonly synthesizeCalls: ReadonlyArray<CommonSynthesizeRequest>
|
|
14
|
+
readonly streamSynthesisCalls: ReadonlyArray<CommonSynthesizeRequest>
|
|
15
|
+
readonly streamSynthesisFromCalls: ReadonlyArray<CommonStreamSynthesizeRequest>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MockSynthesizerScript = {
|
|
19
|
+
/** One blob per `synthesize` call, consumed in order. */
|
|
20
|
+
readonly blobs?: ReadonlyArray<AudioBlob>
|
|
21
|
+
/** One chunk-list per `streamSynthesis` call, consumed in order. */
|
|
22
|
+
readonly streamSynthesisChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
23
|
+
/** One chunk-list per `streamSynthesisFrom` call, consumed in order. */
|
|
24
|
+
readonly streamSynthesisFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const makeService = (
|
|
28
|
+
script: MockSynthesizerScript,
|
|
29
|
+
record: {
|
|
30
|
+
readonly synthesize: (req: CommonSynthesizeRequest) => Effect.Effect<void>
|
|
31
|
+
readonly streamSynthesis: (req: CommonSynthesizeRequest) => Effect.Effect<void>
|
|
32
|
+
readonly streamSynthesisFrom: (req: CommonStreamSynthesizeRequest) => Effect.Effect<void>
|
|
33
|
+
},
|
|
34
|
+
) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const bCursor = yield* Ref.make(0)
|
|
37
|
+
const ssCursor = yield* Ref.make(0)
|
|
38
|
+
const ssfCursor = yield* Ref.make(0)
|
|
39
|
+
const service: SpeechSynthesizerService = {
|
|
40
|
+
synthesize: (request) =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
yield* record.synthesize(request)
|
|
43
|
+
const i = yield* Ref.getAndUpdate(bCursor, (n) => n + 1)
|
|
44
|
+
const scripted = script.blobs ?? []
|
|
45
|
+
if (i >= scripted.length) {
|
|
46
|
+
return yield* Effect.fail(
|
|
47
|
+
new AiError.InvalidRequest({
|
|
48
|
+
provider: "mock",
|
|
49
|
+
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} blobs scripted, but call ${i + 1} was made`,
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
return scripted[i]!
|
|
54
|
+
}),
|
|
55
|
+
streamSynthesis: (request) =>
|
|
56
|
+
Stream.unwrap(
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
yield* record.streamSynthesis(request)
|
|
59
|
+
const i = yield* Ref.getAndUpdate(ssCursor, (n) => n + 1)
|
|
60
|
+
const scripted = script.streamSynthesisChunks ?? []
|
|
61
|
+
if (i >= scripted.length) {
|
|
62
|
+
return Stream.fail(
|
|
63
|
+
new AiError.InvalidRequest({
|
|
64
|
+
provider: "mock",
|
|
65
|
+
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} streamSynthesis lists scripted, but call ${i + 1} was made`,
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
return Stream.fromIterable(scripted[i]!)
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
streamSynthesisFrom: <E, R>(
|
|
73
|
+
textIn: Stream.Stream<string, E, R>,
|
|
74
|
+
request: CommonStreamSynthesizeRequest,
|
|
75
|
+
): Stream.Stream<AudioChunk, AiError.AiError | E, R> =>
|
|
76
|
+
Stream.unwrap(
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
yield* record.streamSynthesisFrom(request)
|
|
79
|
+
const i = yield* Ref.getAndUpdate(ssfCursor, (n) => n + 1)
|
|
80
|
+
const scripted = script.streamSynthesisFromChunks ?? []
|
|
81
|
+
if (i >= scripted.length) {
|
|
82
|
+
const exhausted: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(
|
|
83
|
+
new AiError.InvalidRequest({
|
|
84
|
+
provider: "mock",
|
|
85
|
+
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} streamSynthesisFrom lists scripted, but call ${i + 1} was made`,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
return exhausted
|
|
89
|
+
}
|
|
90
|
+
// Drain the input text fully before emitting scripted audio chunks,
|
|
91
|
+
// so consumers can assert on what text was pushed.
|
|
92
|
+
return Stream.drain(textIn).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
return service
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Layer providing the `SpeechSynthesizer` service AND the
|
|
101
|
+
* `TtsIncrementalText` capability marker. Use for the common case
|
|
102
|
+
* where code under test exercises `streamSynthesisFrom`.
|
|
103
|
+
*/
|
|
104
|
+
export const layer = (
|
|
105
|
+
script: MockSynthesizerScript,
|
|
106
|
+
): {
|
|
107
|
+
readonly layer: Layer.Layer<SpeechSynthesizer | TtsIncrementalText>
|
|
108
|
+
readonly recorder: Effect.Effect<MockSynthesizerRecorder>
|
|
109
|
+
} => {
|
|
110
|
+
const bCalls = Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([])
|
|
111
|
+
const ssCalls = Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([])
|
|
112
|
+
const ssfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamSynthesizeRequest>>([])
|
|
113
|
+
const synthesizerLayer = Layer.effect(
|
|
114
|
+
SpeechSynthesizer,
|
|
115
|
+
makeService(script, {
|
|
116
|
+
synthesize: (req) => Ref.update(bCalls, (xs) => [...xs, req]),
|
|
117
|
+
streamSynthesis: (req) => Ref.update(ssCalls, (xs) => [...xs, req]),
|
|
118
|
+
streamSynthesisFrom: (req) => Ref.update(ssfCalls, (xs) => [...xs, req]),
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
const live = Layer.merge(synthesizerLayer, Layer.succeed(TtsIncrementalText, undefined))
|
|
122
|
+
return {
|
|
123
|
+
layer: live,
|
|
124
|
+
recorder: Effect.gen(function* () {
|
|
125
|
+
const synthesizeCalls = yield* Ref.get(bCalls)
|
|
126
|
+
const streamSynthesisCalls = yield* Ref.get(ssCalls)
|
|
127
|
+
const streamSynthesisFromCalls = yield* Ref.get(ssfCalls)
|
|
128
|
+
return { synthesizeCalls, streamSynthesisCalls, streamSynthesisFromCalls }
|
|
129
|
+
}),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Variant that omits the `TtsIncrementalText` marker — simulates a
|
|
135
|
+
* provider without incremental-text-in support (e.g. OpenAI, AWS
|
|
136
|
+
* Polly non-Generative). Calls to `streamSynthesisFrom` in code under
|
|
137
|
+
* test should be a compile-time error.
|
|
138
|
+
*/
|
|
139
|
+
export const layerWithoutIncremental = (
|
|
140
|
+
script: MockSynthesizerScript,
|
|
141
|
+
): {
|
|
142
|
+
readonly layer: Layer.Layer<SpeechSynthesizer>
|
|
143
|
+
readonly recorder: Effect.Effect<MockSynthesizerRecorder>
|
|
144
|
+
} => {
|
|
145
|
+
const bCalls = Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([])
|
|
146
|
+
const ssCalls = Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([])
|
|
147
|
+
const ssfCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamSynthesizeRequest>>([])
|
|
148
|
+
const live = Layer.effect(
|
|
149
|
+
SpeechSynthesizer,
|
|
150
|
+
makeService(script, {
|
|
151
|
+
synthesize: (req) => Ref.update(bCalls, (xs) => [...xs, req]),
|
|
152
|
+
streamSynthesis: (req) => Ref.update(ssCalls, (xs) => [...xs, req]),
|
|
153
|
+
streamSynthesisFrom: (req) => Ref.update(ssfCalls, (xs) => [...xs, req]),
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
return {
|
|
157
|
+
layer: live,
|
|
158
|
+
recorder: Effect.gen(function* () {
|
|
159
|
+
const synthesizeCalls = yield* Ref.get(bCalls)
|
|
160
|
+
const streamSynthesisCalls = yield* Ref.get(ssCalls)
|
|
161
|
+
const streamSynthesisFromCalls = yield* Ref.get(ssfCalls)
|
|
162
|
+
return { synthesizeCalls, streamSynthesisCalls, streamSynthesisFromCalls }
|
|
163
|
+
}),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Effect, Layer, Ref, Stream } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
import type { TranscriptEvent, TranscriptResult } from "../domain/Transcript.js"
|
|
4
|
+
import {
|
|
5
|
+
SttStreaming,
|
|
6
|
+
Transcriber,
|
|
7
|
+
type CommonStreamTranscribeRequest,
|
|
8
|
+
type CommonTranscribeRequest,
|
|
9
|
+
type TranscriberService,
|
|
10
|
+
} from "../transcriber/Transcriber.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recorder of every call made to the mock.
|
|
14
|
+
*/
|
|
15
|
+
export type MockTranscriberRecorder = {
|
|
16
|
+
readonly transcribeCalls: ReadonlyArray<CommonTranscribeRequest>
|
|
17
|
+
readonly streamCalls: ReadonlyArray<CommonStreamTranscribeRequest>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MockTranscriberScript = {
|
|
21
|
+
/** One result per `transcribe` call, consumed in order. */
|
|
22
|
+
readonly transcripts?: ReadonlyArray<TranscriptResult>
|
|
23
|
+
/** One event-list per `streamTranscriptionFrom` call, consumed in order. */
|
|
24
|
+
readonly streams?: ReadonlyArray<ReadonlyArray<TranscriptEvent>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const makeService = (
|
|
28
|
+
script: MockTranscriberScript,
|
|
29
|
+
record: {
|
|
30
|
+
readonly transcribe: (req: CommonTranscribeRequest) => Effect.Effect<void>
|
|
31
|
+
readonly stream: (req: CommonStreamTranscribeRequest) => Effect.Effect<void>
|
|
32
|
+
},
|
|
33
|
+
) =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const tCursor = yield* Ref.make(0)
|
|
36
|
+
const sCursor = yield* Ref.make(0)
|
|
37
|
+
const service: TranscriberService = {
|
|
38
|
+
transcribe: (request) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
yield* record.transcribe(request)
|
|
41
|
+
const i = yield* Ref.getAndUpdate(tCursor, (n) => n + 1)
|
|
42
|
+
const scripted = script.transcripts ?? []
|
|
43
|
+
if (i >= scripted.length) {
|
|
44
|
+
return yield* Effect.fail(
|
|
45
|
+
new AiError.InvalidRequest({
|
|
46
|
+
provider: "mock",
|
|
47
|
+
raw: `MockTranscriber exhausted: ${scripted.length} transcripts scripted, but call ${i + 1} was made`,
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return scripted[i]!
|
|
52
|
+
}),
|
|
53
|
+
streamTranscriptionFrom: <E, R>(
|
|
54
|
+
audioIn: Stream.Stream<Uint8Array, E, R>,
|
|
55
|
+
request: CommonStreamTranscribeRequest,
|
|
56
|
+
): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>
|
|
57
|
+
Stream.unwrap(
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
yield* record.stream(request)
|
|
60
|
+
const i = yield* Ref.getAndUpdate(sCursor, (n) => n + 1)
|
|
61
|
+
const scripted = script.streams ?? []
|
|
62
|
+
if (i >= scripted.length) {
|
|
63
|
+
const exhausted: Stream.Stream<TranscriptEvent, AiError.AiError | E, R> = Stream.fail(
|
|
64
|
+
new AiError.InvalidRequest({
|
|
65
|
+
provider: "mock",
|
|
66
|
+
raw: `MockTranscriber exhausted: ${scripted.length} streams scripted, but call ${i + 1} was made`,
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
return exhausted
|
|
70
|
+
}
|
|
71
|
+
// Drain the input audio fully before emitting the scripted events,
|
|
72
|
+
// so consumers can assert on what bytes were pushed.
|
|
73
|
+
return Stream.drain(audioIn).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
return service
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns a Layer that provides both the `Transcriber` service and the
|
|
82
|
+
* `SttStreaming` capability marker. Use when the code under test calls
|
|
83
|
+
* `streamTranscriptionFrom`.
|
|
84
|
+
*/
|
|
85
|
+
export const layer = (
|
|
86
|
+
script: MockTranscriberScript,
|
|
87
|
+
): {
|
|
88
|
+
readonly layer: Layer.Layer<Transcriber | SttStreaming>
|
|
89
|
+
readonly recorder: Effect.Effect<MockTranscriberRecorder>
|
|
90
|
+
} => {
|
|
91
|
+
const tCalls = Ref.makeUnsafe<ReadonlyArray<CommonTranscribeRequest>>([])
|
|
92
|
+
const sCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamTranscribeRequest>>([])
|
|
93
|
+
const transcriberLayer = Layer.effect(
|
|
94
|
+
Transcriber,
|
|
95
|
+
makeService(script, {
|
|
96
|
+
transcribe: (req) => Ref.update(tCalls, (xs) => [...xs, req]),
|
|
97
|
+
stream: (req) => Ref.update(sCalls, (xs) => [...xs, req]),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
const live = Layer.merge(transcriberLayer, Layer.succeed(SttStreaming, undefined))
|
|
101
|
+
return {
|
|
102
|
+
layer: live,
|
|
103
|
+
recorder: Effect.gen(function* () {
|
|
104
|
+
const transcribeCalls = yield* Ref.get(tCalls)
|
|
105
|
+
const streamCalls = yield* Ref.get(sCalls)
|
|
106
|
+
return { transcribeCalls, streamCalls }
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Variant that omits the `SttStreaming` marker — use to test that
|
|
113
|
+
* consumers calling `streamTranscriptionFrom` fail to compile against
|
|
114
|
+
* a non-streaming provider.
|
|
115
|
+
*/
|
|
116
|
+
export const layerSyncOnly = (
|
|
117
|
+
script: MockTranscriberScript,
|
|
118
|
+
): {
|
|
119
|
+
readonly layer: Layer.Layer<Transcriber>
|
|
120
|
+
readonly recorder: Effect.Effect<MockTranscriberRecorder>
|
|
121
|
+
} => {
|
|
122
|
+
const tCalls = Ref.makeUnsafe<ReadonlyArray<CommonTranscribeRequest>>([])
|
|
123
|
+
const sCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamTranscribeRequest>>([])
|
|
124
|
+
const live = Layer.effect(
|
|
125
|
+
Transcriber,
|
|
126
|
+
makeService(script, {
|
|
127
|
+
transcribe: (req) => Ref.update(tCalls, (xs) => [...xs, req]),
|
|
128
|
+
stream: (req) => Ref.update(sCalls, (xs) => [...xs, req]),
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
return {
|
|
132
|
+
layer: live,
|
|
133
|
+
recorder: Effect.gen(function* () {
|
|
134
|
+
const transcribeCalls = yield* Ref.get(tCalls)
|
|
135
|
+
const streamCalls = yield* Ref.get(sCalls)
|
|
136
|
+
return { transcribeCalls, streamCalls }
|
|
137
|
+
}),
|
|
138
|
+
}
|
|
139
|
+
}
|