@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.
- package/README.md +1 -1
- package/dist/{Audio-BfCTGnH3.d.mts → Audio-DPlTsNdz.d.mts} +3 -2
- package/dist/Audio-DPlTsNdz.d.mts.map +1 -0
- package/dist/{Items-BH8xUkoR.d.mts → Items-D8UaXQx_.d.mts} +15 -15
- package/dist/Items-D8UaXQx_.d.mts.map +1 -0
- package/dist/SandboxError-BvKypkJm.d.mts +158 -0
- package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
- package/dist/{Tool-87ViKCCO.d.mts → Tool-CiFXYjmR.d.mts} +11 -11
- package/dist/{Tool-87ViKCCO.d.mts.map → Tool-CiFXYjmR.d.mts.map} +1 -1
- package/dist/capabilities/Capabilities.d.mts +78 -0
- package/dist/capabilities/Capabilities.d.mts.map +1 -0
- package/dist/capabilities/Capabilities.mjs +65 -0
- package/dist/capabilities/Capabilities.mjs.map +1 -0
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Audio.mjs.map +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +15 -15
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Music.d.mts +131 -72
- package/dist/domain/Music.d.mts.map +1 -1
- package/dist/domain/Music.mjs +36 -9
- package/dist/domain/Music.mjs.map +1 -1
- package/dist/domain/Transcript.d.mts +8 -2
- package/dist/domain/Transcript.d.mts.map +1 -1
- package/dist/domain/Transcript.mjs.map +1 -1
- package/dist/domain/Turn.d.mts +388 -2
- package/dist/domain/Turn.d.mts.map +1 -0
- package/dist/domain/Turn.mjs +13 -13
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +12 -2
- package/dist/embedding-model/Embedding.d.mts.map +1 -1
- package/dist/embedding-model/Embedding.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +36 -23
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +20 -1
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +15 -3
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +13 -7
- package/dist/index.mjs +9 -3
- package/dist/language-model/LanguageModel.d.mts +5 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +37 -90
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -93
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +46 -54
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +48 -25
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
- package/dist/music-generator/MusicGenerator.mjs +17 -9
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +81 -56
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
- package/dist/sandbox/Memory.d.mts +53 -0
- package/dist/sandbox/Memory.d.mts.map +1 -0
- package/dist/sandbox/Memory.mjs +68 -0
- package/dist/sandbox/Memory.mjs.map +1 -0
- package/dist/sandbox/Sandbox.d.mts +717 -0
- package/dist/sandbox/Sandbox.d.mts.map +1 -0
- package/dist/sandbox/Sandbox.mjs +192 -0
- package/dist/sandbox/Sandbox.mjs.map +1 -0
- package/dist/sandbox/SandboxError.d.mts +2 -0
- package/dist/sandbox/SandboxError.mjs +89 -0
- package/dist/sandbox/SandboxError.mjs.map +1 -0
- package/dist/sandbox/SandboxImage.d.mts +28 -0
- package/dist/sandbox/SandboxImage.d.mts.map +1 -0
- package/dist/sandbox/SandboxImage.mjs +32 -0
- package/dist/sandbox/SandboxImage.mjs.map +1 -0
- package/dist/sandbox/SandboxNetwork.d.mts +34 -0
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
- package/dist/sandbox/SandboxNetwork.mjs +36 -0
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +84 -3
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +59 -5
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +12 -7
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
- package/dist/testing/MockMusicGenerator.mjs +4 -3
- package/dist/testing/MockMusicGenerator.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +3 -3
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +604 -0
- package/dist/testing/MockSandbox.d.mts.map +1 -0
- package/dist/testing/MockSandbox.mjs +160 -0
- package/dist/testing/MockSandbox.mjs.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts +27 -12
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
- package/dist/tool/Approval.d.mts.map +1 -0
- package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
- package/dist/tool/Approval.mjs.map +1 -0
- package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
- package/dist/tool/Approval.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -6
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +5 -5
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +19 -19
- package/dist/tool/ToolEvent.d.mts.map +1 -1
- package/dist/tool/ToolEvent.mjs +8 -8
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
- package/dist/tool/ToolResult.d.mts.map +1 -0
- package/dist/tool/ToolResult.mjs +61 -0
- package/dist/tool/ToolResult.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +58 -26
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +62 -34
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +13 -13
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +11 -10
- package/dist/transcriber/Transcriber.d.mts.map +1 -1
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +3 -3
- package/dist/transcriber/Transcriber.test.mjs.map +1 -1
- package/package.json +35 -7
- package/src/capabilities/Capabilities.ts +81 -0
- package/src/domain/Audio.ts +2 -1
- package/src/domain/Items.ts +9 -9
- package/src/domain/Music.ts +142 -68
- package/src/domain/Transcript.ts +7 -1
- package/src/domain/Turn.ts +16 -16
- package/src/embedding-model/Embedding.ts +11 -0
- package/src/embedding-model/EmbeddingModel.test.ts +15 -4
- package/src/embedding-model/EmbeddingModel.ts +48 -21
- package/src/index.ts +8 -2
- package/src/language-model/LanguageModel.ts +3 -3
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- package/src/music-generator/MusicGenerator.test.ts +78 -53
- package/src/music-generator/MusicGenerator.ts +65 -24
- package/src/sandbox/Memory.ts +128 -0
- package/src/sandbox/Sandbox.ts +639 -0
- package/src/sandbox/SandboxError.ts +140 -0
- package/src/sandbox/SandboxImage.ts +26 -0
- package/src/sandbox/SandboxNetwork.ts +34 -0
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +92 -3
- package/src/speech-synthesizer/SpeechSynthesizer.ts +109 -0
- package/src/testing/MockMusicGenerator.ts +28 -18
- package/src/testing/MockProvider.ts +21 -20
- package/src/testing/MockSandbox.ts +342 -0
- package/src/testing/MockSpeechSynthesizer.ts +137 -77
- package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
- package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
- package/src/tool/HistoryCheck.ts +10 -13
- package/src/tool/Tool.ts +13 -15
- package/src/tool/ToolEvent.ts +7 -7
- package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
- package/src/tool/Toolkit.test.ts +13 -13
- package/src/tool/Toolkit.ts +97 -73
- package/src/transcriber/Transcriber.test.ts +3 -3
- package/src/transcriber/Transcriber.ts +10 -7
- package/dist/Audio-BfCTGnH3.d.mts.map +0 -1
- package/dist/Items-BH8xUkoR.d.mts.map +0 -1
- package/dist/Turn-0CwCAyVe.d.mts +0 -388
- package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
- package/dist/tool/Outcome.d.mts.map +0 -1
- package/dist/tool/Outcome.mjs +0 -61
- package/dist/tool/Outcome.mjs.map +0 -1
- package/dist/tool/Resolvers.d.mts.map +0 -1
- package/dist/tool/Resolvers.mjs.map +0 -1
- package/dist/tool/Resolvers.test.mjs.map +0 -1
- /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { Effect, Stream } from "effect"
|
|
1
|
+
import { Duration, Effect, Stream } from "effect"
|
|
2
2
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
3
3
|
import type * as AiError from "../domain/AiError.js"
|
|
4
4
|
import type { AudioChunk, AudioFormat } from "../domain/Audio.js"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
audioEvent,
|
|
7
|
+
controlInput,
|
|
8
|
+
type GenerateResult,
|
|
9
|
+
type MusicResult,
|
|
10
|
+
type MusicStreamEvent,
|
|
11
|
+
promptsInput,
|
|
12
|
+
singleVariant,
|
|
13
|
+
} from "../domain/Music.js"
|
|
6
14
|
import * as MockMusicGenerator from "../testing/MockMusicGenerator.js"
|
|
7
15
|
import * as MusicGenerator from "./MusicGenerator.js"
|
|
8
16
|
|
|
@@ -13,50 +21,69 @@ const mp3Format: AudioFormat = {
|
|
|
13
21
|
channels: 2,
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
const oneResult: MusicResult = {
|
|
25
|
+
audio: {
|
|
26
|
+
format: mp3Format,
|
|
27
|
+
bytes: new Uint8Array([0xff, 0xfb, 0x90, 0x00]),
|
|
28
|
+
duration: Duration.seconds(30),
|
|
29
|
+
},
|
|
30
|
+
provider: "mock-music",
|
|
20
31
|
lyrics: "[Verse]\nhello\n",
|
|
21
|
-
watermark:
|
|
32
|
+
watermark: "synthid",
|
|
22
33
|
}
|
|
23
34
|
|
|
35
|
+
const oneVariant: GenerateResult = singleVariant(oneResult)
|
|
36
|
+
|
|
24
37
|
const chunk = (n: number): AudioChunk => ({ bytes: new Uint8Array([n]) })
|
|
25
38
|
|
|
26
39
|
describe("MusicGenerator.generate", () => {
|
|
27
|
-
it("returns the scripted
|
|
28
|
-
const mock = MockMusicGenerator.layer({ results: [
|
|
40
|
+
it("returns the scripted GenerateResult", async () => {
|
|
41
|
+
const mock = MockMusicGenerator.layer({ results: [oneVariant] })
|
|
29
42
|
const program = MusicGenerator.generate({
|
|
30
43
|
model: "mock-music",
|
|
31
|
-
|
|
44
|
+
prompt: "upbeat indie pop",
|
|
32
45
|
})
|
|
33
46
|
const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
34
|
-
expect(out.bytes).toEqual(
|
|
35
|
-
expect(out.
|
|
36
|
-
expect(out.watermark
|
|
37
|
-
expect(out.lyrics).toContain("[Verse]")
|
|
47
|
+
expect(out.primary.audio.bytes).toEqual(oneResult.audio.bytes)
|
|
48
|
+
expect(out.primary.audio.duration).toEqual(Duration.seconds(30))
|
|
49
|
+
expect(out.primary.watermark).toBe("synthid")
|
|
50
|
+
expect(out.primary.lyrics).toContain("[Verse]")
|
|
51
|
+
expect(out.variants).toHaveLength(1)
|
|
52
|
+
expect(out.variants[0]).toBe(out.primary)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("exposes multiple variants when the provider returned more than one", async () => {
|
|
56
|
+
const secondTrack: MusicResult = {
|
|
57
|
+
...oneResult,
|
|
58
|
+
audio: { ...oneResult.audio, bytes: new Uint8Array([0xff, 0xfb, 0xa0, 0x00]) },
|
|
59
|
+
}
|
|
60
|
+
const twoVariants: GenerateResult = {
|
|
61
|
+
primary: oneResult,
|
|
62
|
+
variants: [oneResult, secondTrack],
|
|
63
|
+
}
|
|
64
|
+
const mock = MockMusicGenerator.layer({ results: [twoVariants] })
|
|
65
|
+
const program = MusicGenerator.generate({ model: "m", prompt: "house" })
|
|
66
|
+
const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
67
|
+
expect(out.variants).toHaveLength(2)
|
|
68
|
+
expect(out.variants[1]!.audio.bytes).toEqual(secondTrack.audio.bytes)
|
|
38
69
|
})
|
|
39
70
|
|
|
40
71
|
it("records the request shape on the recorder", async () => {
|
|
41
|
-
const mock = MockMusicGenerator.layer({ results: [
|
|
72
|
+
const mock = MockMusicGenerator.layer({ results: [oneVariant, oneVariant] })
|
|
42
73
|
const program = Effect.gen(function* () {
|
|
43
|
-
yield* MusicGenerator.generate({ model: "m",
|
|
74
|
+
yield* MusicGenerator.generate({ model: "m", prompt: "techno" })
|
|
44
75
|
yield* MusicGenerator.generate({
|
|
45
76
|
model: "m",
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
],
|
|
50
|
-
bpm: 120,
|
|
51
|
-
instrumental: true,
|
|
77
|
+
prompt: "synthwave",
|
|
78
|
+
duration: Duration.seconds(45),
|
|
79
|
+
seed: 42,
|
|
52
80
|
})
|
|
53
81
|
return yield* mock.recorder
|
|
54
82
|
})
|
|
55
83
|
const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
56
84
|
expect(rec.generateCalls.length).toBe(2)
|
|
57
|
-
expect(rec.generateCalls[1]!.
|
|
58
|
-
expect(rec.generateCalls[1]!.
|
|
59
|
-
expect(Array.isArray(rec.generateCalls[1]!.prompts)).toBe(true)
|
|
85
|
+
expect(rec.generateCalls[1]!.duration).toEqual(Duration.seconds(45))
|
|
86
|
+
expect(rec.generateCalls[1]!.seed).toBe(42)
|
|
60
87
|
})
|
|
61
88
|
})
|
|
62
89
|
|
|
@@ -66,7 +93,7 @@ describe("MusicGenerator.streamGeneration", () => {
|
|
|
66
93
|
streamGenerationChunks: [[chunk(1), chunk(2), chunk(3)]],
|
|
67
94
|
})
|
|
68
95
|
const program = Stream.runCollect(
|
|
69
|
-
MusicGenerator.streamGeneration({ model: "m",
|
|
96
|
+
MusicGenerator.streamGeneration({ model: "m", prompt: "ambient" }),
|
|
70
97
|
)
|
|
71
98
|
const out = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
72
99
|
expect(out.map((c) => Array.from(c.bytes))).toEqual([[1], [2], [3]])
|
|
@@ -76,15 +103,15 @@ describe("MusicGenerator.streamGeneration", () => {
|
|
|
76
103
|
describe("MusicGenerator capability marker (compile-time)", () => {
|
|
77
104
|
const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {
|
|
78
105
|
model: "m",
|
|
79
|
-
|
|
106
|
+
prompt: "",
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
it("requires `MusicInteractiveSession` on the R channel of streamGenerationFrom", () => {
|
|
83
110
|
const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
|
|
84
|
-
const
|
|
85
|
-
expectTypeOf(
|
|
111
|
+
const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
|
|
112
|
+
expectTypeOf(events).toEqualTypeOf<
|
|
86
113
|
Stream.Stream<
|
|
87
|
-
|
|
114
|
+
MusicStreamEvent,
|
|
88
115
|
AiError.AiError,
|
|
89
116
|
MusicGenerator.MusicGenerator | MusicGenerator.MusicInteractiveSession
|
|
90
117
|
>
|
|
@@ -92,14 +119,14 @@ describe("MusicGenerator capability marker (compile-time)", () => {
|
|
|
92
119
|
})
|
|
93
120
|
|
|
94
121
|
it("does NOT require `MusicInteractiveSession` for sync `generate`", () => {
|
|
95
|
-
const eff = MusicGenerator.generate({ model: "m",
|
|
122
|
+
const eff = MusicGenerator.generate({ model: "m", prompt: "ambient" })
|
|
96
123
|
expectTypeOf(eff).toEqualTypeOf<
|
|
97
|
-
Effect.Effect<
|
|
124
|
+
Effect.Effect<GenerateResult, AiError.AiError, MusicGenerator.MusicGenerator>
|
|
98
125
|
>()
|
|
99
126
|
})
|
|
100
127
|
|
|
101
128
|
it("does NOT require `MusicInteractiveSession` for `streamGeneration`", () => {
|
|
102
|
-
const audio = MusicGenerator.streamGeneration({ model: "m",
|
|
129
|
+
const audio = MusicGenerator.streamGeneration({ model: "m", prompt: "ambient" })
|
|
103
130
|
expectTypeOf(audio).toEqualTypeOf<
|
|
104
131
|
Stream.Stream<AudioChunk, AiError.AiError, MusicGenerator.MusicGenerator>
|
|
105
132
|
>()
|
|
@@ -108,8 +135,8 @@ describe("MusicGenerator capability marker (compile-time)", () => {
|
|
|
108
135
|
it("a layer without the marker leaves `MusicInteractiveSession` unsatisfied in R", () => {
|
|
109
136
|
const noMarker = MockMusicGenerator.layerWithoutInteractive({})
|
|
110
137
|
const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
|
|
111
|
-
const
|
|
112
|
-
const program = Stream.runDrain(
|
|
138
|
+
const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
|
|
139
|
+
const program = Stream.runDrain(events).pipe(Effect.provide(noMarker.layer))
|
|
113
140
|
expectTypeOf(program).toEqualTypeOf<
|
|
114
141
|
Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>
|
|
115
142
|
>()
|
|
@@ -117,53 +144,51 @@ describe("MusicGenerator capability marker (compile-time)", () => {
|
|
|
117
144
|
|
|
118
145
|
it("a full layer (with marker) clears R to never", () => {
|
|
119
146
|
const fullMock = MockMusicGenerator.layer({
|
|
120
|
-
|
|
147
|
+
streamGenerationFromEvents: [[]],
|
|
121
148
|
})
|
|
122
149
|
const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
|
|
123
|
-
const
|
|
124
|
-
const program = Stream.runDrain(
|
|
150
|
+
const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
|
|
151
|
+
const program = Stream.runDrain(events).pipe(Effect.provide(fullMock.layer))
|
|
125
152
|
expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()
|
|
126
153
|
})
|
|
127
154
|
})
|
|
128
155
|
|
|
129
156
|
describe("MusicGenerator.streamGenerationFrom", () => {
|
|
130
157
|
const sgfReq: MusicGenerator.CommonStreamGenerateMusicRequest = {
|
|
131
|
-
model: "lyria-realtime-
|
|
132
|
-
|
|
158
|
+
model: "lyria-realtime-exp",
|
|
159
|
+
prompt: "",
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
it("drains a session-input stream and emits scripted
|
|
162
|
+
it("drains a session-input stream and emits scripted events", async () => {
|
|
136
163
|
const mock = MockMusicGenerator.layer({
|
|
137
|
-
|
|
164
|
+
streamGenerationFromEvents: [[audioEvent(chunk(10)), audioEvent(chunk(20))]],
|
|
138
165
|
})
|
|
139
166
|
const inputs = Stream.fromIterable([
|
|
140
167
|
promptsInput([{ text: "minimal techno", weight: 1.0 }]),
|
|
141
|
-
|
|
168
|
+
controlInput("play"),
|
|
142
169
|
promptsInput([
|
|
143
170
|
{ text: "minimal techno", weight: 1.0 },
|
|
144
171
|
{ text: "1980s synthwave", weight: 0.3 },
|
|
145
172
|
]),
|
|
146
173
|
])
|
|
147
|
-
const
|
|
148
|
-
const out = await Effect.runPromise(Stream.runCollect(
|
|
149
|
-
|
|
174
|
+
const events = inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq))
|
|
175
|
+
const out = await Effect.runPromise(Stream.runCollect(events).pipe(Effect.provide(mock.layer)))
|
|
176
|
+
const audioChunks = out.flatMap((e) => (e._tag === "audio" ? [e.chunk] : []))
|
|
177
|
+
expect(audioChunks.map((c) => Array.from(c.bytes))).toEqual([[10], [20]])
|
|
150
178
|
})
|
|
151
179
|
|
|
152
180
|
it("records the request on the streamGenerationFrom call channel", async () => {
|
|
153
181
|
const mock = MockMusicGenerator.layer({
|
|
154
|
-
|
|
182
|
+
streamGenerationFromEvents: [[audioEvent(chunk(42))]],
|
|
155
183
|
})
|
|
156
184
|
const program = Effect.gen(function* () {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
MusicGenerator.streamGenerationFrom(sgfReq),
|
|
160
|
-
),
|
|
161
|
-
)
|
|
185
|
+
const inputs = Stream.fromIterable([promptsInput([{ text: "x" }])])
|
|
186
|
+
yield* Stream.runDrain(inputs.pipe(MusicGenerator.streamGenerationFrom(sgfReq)))
|
|
162
187
|
return yield* mock.recorder
|
|
163
188
|
})
|
|
164
189
|
const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
165
190
|
expect(rec.streamGenerationFromCalls.length).toBe(1)
|
|
166
|
-
expect(rec.streamGenerationFromCalls[0]!.model).toBe("lyria-realtime-
|
|
191
|
+
expect(rec.streamGenerationFromCalls[0]!.model).toBe("lyria-realtime-exp")
|
|
167
192
|
expect(rec.generateCalls.length).toBe(0)
|
|
168
193
|
expect(rec.streamGenerationCalls.length).toBe(0)
|
|
169
194
|
})
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { Context, Effect, Function, Stream } from "effect"
|
|
2
2
|
import * as AiError from "../domain/AiError.js"
|
|
3
|
-
import type { AudioChunk } from "../domain/Audio.js"
|
|
4
3
|
import type {
|
|
5
4
|
CommonGenerateMusicRequest,
|
|
6
5
|
CommonStreamGenerateMusicRequest,
|
|
7
|
-
|
|
6
|
+
GenerateResult,
|
|
8
7
|
MusicSessionInput,
|
|
8
|
+
MusicStreamEvent,
|
|
9
9
|
} from "../domain/Music.js"
|
|
10
|
+
import type { AudioChunk } from "../domain/Audio.js"
|
|
10
11
|
|
|
11
12
|
export type {
|
|
12
13
|
CommonGenerateMusicRequest,
|
|
13
14
|
CommonStreamGenerateMusicRequest,
|
|
15
|
+
GenerateResult,
|
|
14
16
|
MusicResult,
|
|
17
|
+
MusicSection,
|
|
18
|
+
MusicSessionControl,
|
|
15
19
|
MusicSessionInput,
|
|
20
|
+
MusicStreamEvent,
|
|
21
|
+
Watermark,
|
|
16
22
|
WeightedPrompt,
|
|
17
23
|
} from "../domain/Music.js"
|
|
18
24
|
|
|
@@ -20,35 +26,50 @@ export type MusicGeneratorService = {
|
|
|
20
26
|
/**
|
|
21
27
|
* One-shot. Prompt in, full audio bytes out. Universally supported.
|
|
22
28
|
* Async/poll-based providers (Suno, Mureka) hide their poll loop
|
|
23
|
-
* inside the adapter
|
|
29
|
+
* inside the adapter, caller still sees a single `Effect`.
|
|
30
|
+
*
|
|
31
|
+
* Returns `GenerateResult` with `primary` plus a `variants` array.
|
|
32
|
+
* Suno and Mureka always return 2 tracks; every other provider
|
|
33
|
+
* returns 1 (and `primary === variants[0]`).
|
|
24
34
|
*/
|
|
25
35
|
readonly generate: (
|
|
26
36
|
request: CommonGenerateMusicRequest,
|
|
27
|
-
) => Effect.Effect<
|
|
37
|
+
) => Effect.Effect<GenerateResult, AiError.AiError>
|
|
28
38
|
/**
|
|
29
39
|
* Prompt in, audio chunks streamed out. Providers without a native
|
|
30
40
|
* chunked-output endpoint (Lyria 3 sync, Mureka, MiniMax, Stable
|
|
31
41
|
* Audio) emulate this by calling `generate` and emitting a single
|
|
32
|
-
* `AudioChunk
|
|
42
|
+
* `AudioChunk`, first-class, no `Unsupported`.
|
|
33
43
|
*/
|
|
34
44
|
readonly streamGeneration: (
|
|
35
45
|
request: CommonStreamGenerateMusicRequest,
|
|
36
46
|
) => Stream.Stream<AudioChunk, AiError.AiError>
|
|
37
47
|
/**
|
|
38
|
-
* Bidirectional session: a `Stream` of
|
|
39
|
-
* in, a `Stream` of
|
|
40
|
-
*
|
|
41
|
-
* finalized via `Stream.scoped`.
|
|
48
|
+
* Bidirectional session: a `Stream` of `MusicSessionInput` (prompts
|
|
49
|
+
* + playback control) flows in, a `Stream` of `MusicStreamEvent`
|
|
50
|
+
* flows out. The session WS / RPC is acquired on first pull and
|
|
51
|
+
* released when the output stream is finalized via `Stream.scoped`.
|
|
52
|
+
*
|
|
53
|
+
* Cross-provider callers send `prompts` (steer the active
|
|
54
|
+
* generation) and `control` (play / pause / stop / reset) — the two
|
|
55
|
+
* actions that converge across interactive-media protocols.
|
|
56
|
+
* Provider-specific model knobs (Lyria RealTime's density /
|
|
57
|
+
* brightness / mute-stems / BPM / scale, etc.) live on the
|
|
58
|
+
* provider-typed service (`LyriaRealtimeGenerator.streamGenerationFrom`),
|
|
59
|
+
* which extends this union with its own `config` variant.
|
|
60
|
+
*
|
|
61
|
+
* Output is `MusicStreamEvent`: `audio` chunks alongside in-band
|
|
62
|
+
* `warning` and `filteredPrompt` events the model emits server-side.
|
|
42
63
|
*
|
|
43
64
|
* Gated by the `MusicInteractiveSession` capability marker on the
|
|
44
|
-
* top-level helper
|
|
65
|
+
* top-level helper. Providers without bidirectional support don't
|
|
45
66
|
* ship the marker, so calls fail at `Effect.provide` with a type
|
|
46
67
|
* error.
|
|
47
68
|
*/
|
|
48
69
|
readonly streamGenerationFrom: <E, R>(
|
|
49
70
|
input: Stream.Stream<MusicSessionInput, E, R>,
|
|
50
71
|
request: CommonStreamGenerateMusicRequest,
|
|
51
|
-
) => Stream.Stream<
|
|
72
|
+
) => Stream.Stream<MusicStreamEvent, AiError.AiError | E, R>
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
export class MusicGenerator extends Context.Service<MusicGenerator, MusicGeneratorService>()(
|
|
@@ -56,23 +77,23 @@ export class MusicGenerator extends Context.Service<MusicGenerator, MusicGenerat
|
|
|
56
77
|
) {}
|
|
57
78
|
|
|
58
79
|
/**
|
|
59
|
-
* Capability marker
|
|
80
|
+
* Capability marker, provided by provider layers whose
|
|
60
81
|
* `streamGenerationFrom` is wired up at the wire level. Currently only
|
|
61
82
|
* Lyria RealTime (via the BidiGenerateMusic WebSocket) ships it.
|
|
62
83
|
* Calling `streamGenerationFrom` while only a non-interactive Layer is
|
|
63
84
|
* in scope fails at `Effect.provide` with a type error.
|
|
64
85
|
*
|
|
65
|
-
* Phantom
|
|
86
|
+
* Phantom: the value is `void`; providers register with
|
|
66
87
|
* `Layer.succeed(MusicInteractiveSession, undefined)`.
|
|
67
88
|
*/
|
|
68
89
|
export class MusicInteractiveSession extends Context.Service<MusicInteractiveSession, void>()(
|
|
69
90
|
"@betalyra/effect-uai/capability/MusicInteractiveSession",
|
|
70
91
|
) {}
|
|
71
92
|
|
|
72
|
-
/** One-shot generation. */
|
|
93
|
+
/** One-shot generation. Returns the full `GenerateResult`. */
|
|
73
94
|
export const generate = (
|
|
74
95
|
request: CommonGenerateMusicRequest,
|
|
75
|
-
): Effect.Effect<
|
|
96
|
+
): Effect.Effect<GenerateResult, AiError.AiError, MusicGenerator> =>
|
|
76
97
|
Effect.flatMap(MusicGenerator.asEffect(), (s) => s.generate(request))
|
|
77
98
|
|
|
78
99
|
/** Prompt in, audio chunks out. */
|
|
@@ -83,17 +104,25 @@ export const streamGeneration = (
|
|
|
83
104
|
|
|
84
105
|
/**
|
|
85
106
|
* Bidirectional generation. Dual-arity: pipeable (data-last) and
|
|
86
|
-
* direct (data-first). Requires `MusicInteractiveSession` in R
|
|
107
|
+
* direct (data-first). Requires `MusicInteractiveSession` in R,
|
|
87
108
|
* providers without bidirectional support are a type error at provide
|
|
88
109
|
* time.
|
|
89
110
|
*
|
|
111
|
+
* On the cross-provider surface, `input` is `Stream<unknown>` and
|
|
112
|
+
* output is `Stream<MusicStreamEvent>`. Use the provider-typed
|
|
113
|
+
* service (`LyriaRealtimeGenerator`) when you want to construct
|
|
114
|
+
* typed session messages.
|
|
115
|
+
*
|
|
90
116
|
* @example
|
|
91
117
|
* ```ts
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
118
|
+
* // Generic surface (opaque input):
|
|
119
|
+
* const audio = inputs.pipe(
|
|
120
|
+
* MusicGenerator.streamGenerationFrom({ model: "lyria-realtime-exp", prompt: "" }),
|
|
121
|
+
* )
|
|
122
|
+
*
|
|
123
|
+
* // Lyria-typed surface (full config knobs):
|
|
124
|
+
* const audio = inputs.pipe(
|
|
125
|
+
* LyriaRealtimeGenerator.streamGenerationFrom({ model: "lyria-realtime-exp", prompt: "" }),
|
|
97
126
|
* )
|
|
98
127
|
* ```
|
|
99
128
|
*/
|
|
@@ -102,17 +131,29 @@ export const streamGenerationFrom: {
|
|
|
102
131
|
request: CommonStreamGenerateMusicRequest,
|
|
103
132
|
): <E, R>(
|
|
104
133
|
input: Stream.Stream<MusicSessionInput, E, R>,
|
|
105
|
-
) => Stream.Stream<
|
|
134
|
+
) => Stream.Stream<
|
|
135
|
+
MusicStreamEvent,
|
|
136
|
+
AiError.AiError | E,
|
|
137
|
+
R | MusicGenerator | MusicInteractiveSession
|
|
138
|
+
>
|
|
106
139
|
<E, R>(
|
|
107
140
|
input: Stream.Stream<MusicSessionInput, E, R>,
|
|
108
141
|
request: CommonStreamGenerateMusicRequest,
|
|
109
|
-
): Stream.Stream<
|
|
142
|
+
): Stream.Stream<
|
|
143
|
+
MusicStreamEvent,
|
|
144
|
+
AiError.AiError | E,
|
|
145
|
+
R | MusicGenerator | MusicInteractiveSession
|
|
146
|
+
>
|
|
110
147
|
} = Function.dual(
|
|
111
148
|
2,
|
|
112
149
|
<E, R>(
|
|
113
150
|
input: Stream.Stream<MusicSessionInput, E, R>,
|
|
114
151
|
request: CommonStreamGenerateMusicRequest,
|
|
115
|
-
)
|
|
152
|
+
): Stream.Stream<
|
|
153
|
+
MusicStreamEvent,
|
|
154
|
+
AiError.AiError | E,
|
|
155
|
+
R | MusicGenerator | MusicInteractiveSession
|
|
156
|
+
> =>
|
|
116
157
|
Stream.unwrap(
|
|
117
158
|
Effect.gen(function* () {
|
|
118
159
|
const s = yield* MusicGenerator.asEffect()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Brand } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A memory / data size, always represented internally as a number of
|
|
5
|
+
* bytes. Mirrors the `Duration` shape from `effect` — a branded value
|
|
6
|
+
* type with a wider `Input` union for ergonomic construction at call
|
|
7
|
+
* sites.
|
|
8
|
+
*
|
|
9
|
+
* `effect` itself has no byte-count primitive (only `Duration`), so we
|
|
10
|
+
* own this here rather than leak provider-specific shapes (Deno's
|
|
11
|
+
* template-literal `Memory`, Vercel's `mb`, Modal's `gpuMemory`, …)
|
|
12
|
+
* through the cross-provider `Sandbox` request types.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import * as Memory from "@effect-uai/core/Memory"
|
|
17
|
+
*
|
|
18
|
+
* Memory.gib(2) // 2 GiB → 2_147_483_648 bytes
|
|
19
|
+
* Memory.fromInputUnsafe("1.5 GiB") // human-string parsing
|
|
20
|
+
* Memory.fromInputUnsafe(1_280_000_000) // raw bytes
|
|
21
|
+
* Memory.toBytes(m) // unwrap
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type Memory = Brand.Branded<number, "Memory">
|
|
26
|
+
|
|
27
|
+
const make = Brand.nominal<Memory>()
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Input — what the public APIs accept.
|
|
31
|
+
//
|
|
32
|
+
// Supported string forms (case-insensitive on the unit, whitespace
|
|
33
|
+
// optional between number and unit):
|
|
34
|
+
//
|
|
35
|
+
// "<n>" bytes (no unit)
|
|
36
|
+
// "<n> B" bytes
|
|
37
|
+
// "<n> kB" 1_000 bytes (decimal)
|
|
38
|
+
// "<n> MB" 1_000_000 (decimal)
|
|
39
|
+
// "<n> GB" 1_000_000_000 (decimal)
|
|
40
|
+
// "<n> KiB" 1_024 (binary)
|
|
41
|
+
// "<n> MiB" 1_048_576 (binary)
|
|
42
|
+
// "<n> GiB" 1_073_741_824 (binary)
|
|
43
|
+
//
|
|
44
|
+
// Decimal vs binary follows the SI / IEC convention (`MB` = 10^6,
|
|
45
|
+
// `MiB` = 2^20). When in doubt, prefer the binary `*iB` units.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export type Input = number | Memory | string
|
|
49
|
+
|
|
50
|
+
const UNIT_BYTES: Record<string, number> = {
|
|
51
|
+
b: 1,
|
|
52
|
+
kb: 1_000,
|
|
53
|
+
mb: 1_000_000,
|
|
54
|
+
gb: 1_000_000_000,
|
|
55
|
+
tb: 1_000_000_000_000,
|
|
56
|
+
kib: 1_024,
|
|
57
|
+
mib: 1_024 * 1_024,
|
|
58
|
+
gib: 1_024 * 1_024 * 1_024,
|
|
59
|
+
tib: 1_024 * 1_024 * 1_024 * 1_024,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PARSE_RE = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]*)\s*$/
|
|
63
|
+
|
|
64
|
+
const parseString = (s: string): number => {
|
|
65
|
+
const m = PARSE_RE.exec(s)
|
|
66
|
+
if (m === null) {
|
|
67
|
+
throw new Error(`Memory: cannot parse "${s}" — expected forms like "1024", "10 MB", "1GiB"`)
|
|
68
|
+
}
|
|
69
|
+
const value = Number(m[1])
|
|
70
|
+
const unit = (m[2] ?? "").toLowerCase()
|
|
71
|
+
if (unit === "") return value
|
|
72
|
+
const factor = UNIT_BYTES[unit]
|
|
73
|
+
if (factor === undefined) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Memory: unknown unit "${m[2]}" in "${s}" — supported: ${Object.keys(UNIT_BYTES).join(", ")}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return value * factor
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Coerce a {@link Input} to a {@link Memory}. Throws on malformed
|
|
83
|
+
* strings or non-finite numbers. Mirrors `Duration.fromInputUnsafe`.
|
|
84
|
+
*/
|
|
85
|
+
export const fromInputUnsafe = (input: Input): Memory => {
|
|
86
|
+
if (typeof input === "number") {
|
|
87
|
+
if (!Number.isFinite(input) || input < 0) {
|
|
88
|
+
throw new Error(`Memory: bytes must be a non-negative finite number, got ${input}`)
|
|
89
|
+
}
|
|
90
|
+
return make(Math.floor(input))
|
|
91
|
+
}
|
|
92
|
+
return make(Math.floor(parseString(input)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Unwrap
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** Return the underlying byte count. */
|
|
100
|
+
export const toBytes = (m: Memory | Input): number =>
|
|
101
|
+
typeof m === "number" && (m as number) === Math.floor(m as number) && (m as number) >= 0
|
|
102
|
+
? (m as number)
|
|
103
|
+
: (fromInputUnsafe(m) as unknown as number)
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Unit constructors — terse for use at call sites.
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/** N bytes. */
|
|
110
|
+
export const bytes = (n: number): Memory => make(Math.floor(n))
|
|
111
|
+
|
|
112
|
+
/** N KiB (binary kibibytes, `n * 1024`). */
|
|
113
|
+
export const kib = (n: number): Memory => make(Math.floor(n * 1_024))
|
|
114
|
+
|
|
115
|
+
/** N MiB (binary mebibytes, `n * 1024^2`). */
|
|
116
|
+
export const mib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024))
|
|
117
|
+
|
|
118
|
+
/** N GiB (binary gibibytes, `n * 1024^3`). */
|
|
119
|
+
export const gib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024 * 1_024))
|
|
120
|
+
|
|
121
|
+
/** N kB (decimal kilobytes, `n * 1000`). */
|
|
122
|
+
export const kb = (n: number): Memory => make(Math.floor(n * 1_000))
|
|
123
|
+
|
|
124
|
+
/** N MB (decimal megabytes, `n * 1_000_000`). */
|
|
125
|
+
export const mb = (n: number): Memory => make(Math.floor(n * 1_000_000))
|
|
126
|
+
|
|
127
|
+
/** N GB (decimal gigabytes, `n * 1_000_000_000`). */
|
|
128
|
+
export const gb = (n: number): Memory => make(Math.floor(n * 1_000_000_000))
|