@deadair/plugin-sdk 0.2.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/LICENSE +21 -0
- package/README.md +1062 -0
- package/dist/article.parse.d.ts +82 -0
- package/dist/article.parse.d.ts.map +1 -0
- package/dist/boundary.json.safe.d.ts +191 -0
- package/dist/boundary.json.safe.d.ts.map +1 -0
- package/dist/capabilities/analysis.d.ts +330 -0
- package/dist/capabilities/analysis.d.ts.map +1 -0
- package/dist/capabilities/charts.d.ts +134 -0
- package/dist/capabilities/charts.d.ts.map +1 -0
- package/dist/capabilities/enrichment.d.ts +255 -0
- package/dist/capabilities/enrichment.d.ts.map +1 -0
- package/dist/capabilities/llm.d.ts +318 -0
- package/dist/capabilities/llm.d.ts.map +1 -0
- package/dist/capabilities/mixer.d.ts +183 -0
- package/dist/capabilities/mixer.d.ts.map +1 -0
- package/dist/capabilities/music.provider.d.ts +245 -0
- package/dist/capabilities/music.provider.d.ts.map +1 -0
- package/dist/capabilities/news.d.ts +171 -0
- package/dist/capabilities/news.d.ts.map +1 -0
- package/dist/capabilities/scrobble.d.ts +133 -0
- package/dist/capabilities/scrobble.d.ts.map +1 -0
- package/dist/capabilities/search.d.ts +122 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/similarity.d.ts +101 -0
- package/dist/capabilities/similarity.d.ts.map +1 -0
- package/dist/capabilities/speech.d.ts +211 -0
- package/dist/capabilities/speech.d.ts.map +1 -0
- package/dist/capabilities/weather.d.ts +192 -0
- package/dist/capabilities/weather.d.ts.map +1 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/define.plugin.d.ts +56 -0
- package/dist/define.plugin.d.ts.map +1 -0
- package/dist/feed.parse.d.ts +97 -0
- package/dist/feed.parse.d.ts.map +1 -0
- package/dist/html.text.d.ts +71 -0
- package/dist/html.text.d.ts.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1153 -0
- package/dist/index.js.map +1 -0
- package/dist/match.text.d.ts +28 -0
- package/dist/match.text.d.ts.map +1 -0
- package/dist/plugin.api.version.d.ts +9 -0
- package/dist/plugin.api.version.d.ts.map +1 -0
- package/dist/plugin.base.d.ts +80 -0
- package/dist/plugin.base.d.ts.map +1 -0
- package/dist/plugin.config.fields.d.ts +524 -0
- package/dist/plugin.config.fields.d.ts.map +1 -0
- package/dist/plugin.config.read.d.ts +52 -0
- package/dist/plugin.config.read.d.ts.map +1 -0
- package/dist/plugin.error.d.ts +177 -0
- package/dist/plugin.error.d.ts.map +1 -0
- package/dist/plugin.host.d.ts +220 -0
- package/dist/plugin.host.d.ts.map +1 -0
- package/dist/plugin.host.response.d.ts +42 -0
- package/dist/plugin.host.response.d.ts.map +1 -0
- package/dist/plugin.http.d.ts +80 -0
- package/dist/plugin.http.d.ts.map +1 -0
- package/dist/plugin.lifecycle.d.ts +68 -0
- package/dist/plugin.lifecycle.d.ts.map +1 -0
- package/dist/plugin.manifest.d.ts +282 -0
- package/dist/plugin.manifest.d.ts.map +1 -0
- package/dist/plugin.permissions.d.ts +186 -0
- package/dist/plugin.permissions.d.ts.map +1 -0
- package/dist/testing/fake.plugin.host.d.ts +85 -0
- package/dist/testing/fake.plugin.host.d.ts.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +141 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `llm` capability. A language-model plugin takes a conversation and answers
|
|
3
|
+
* with words: the line a DJ says, the running order a set generator asked for,
|
|
4
|
+
* the copy for a sponsor read.
|
|
5
|
+
*
|
|
6
|
+
* ## This is a transport, not a writer
|
|
7
|
+
*
|
|
8
|
+
* Nothing here knows what a break is. That is deliberate and it is the one
|
|
9
|
+
* constraint worth defending: the previous station had five things that produced
|
|
10
|
+
* a script (a talk break, a sign-on, a news bulletin, a DJ set, a two-voice
|
|
11
|
+
* dialogue) and every one of them was messages in and text out. A capability
|
|
12
|
+
* shaped around any single one of them has to be reshaped for the next.
|
|
13
|
+
*
|
|
14
|
+
* So: {@link LlmMessage}s in, text out, and everything about WHAT to say lives
|
|
15
|
+
* on the station's side of the fence.
|
|
16
|
+
*
|
|
17
|
+
* ## The words come back as a stream
|
|
18
|
+
*
|
|
19
|
+
* {@link LlmPluginInstance.generate} answers with a handle carrying a stream, the
|
|
20
|
+
* way `speak()` does, and for a stronger reason than memory. The host serializes
|
|
21
|
+
* generations through a single slot, and it holds that slot until the words stop
|
|
22
|
+
* arriving rather than until the call resolves. On a local model, releasing early
|
|
23
|
+
* lets two generations overlap and both of them get slower.
|
|
24
|
+
*
|
|
25
|
+
* Read {@link LlmHandle.text} to the end, or `cancel()` it. Then await
|
|
26
|
+
* {@link LlmHandle.result}, which is where the tool calls, the usage and the
|
|
27
|
+
* reason it stopped are. {@link collectGeneration} does both for a caller that
|
|
28
|
+
* only wants the answer.
|
|
29
|
+
*
|
|
30
|
+
* ## The model is chosen per call
|
|
31
|
+
*
|
|
32
|
+
* {@link LlmRequest.model} overrides whatever the plugin has configured, because
|
|
33
|
+
* a station wants a big model for a show and a small one for a station ident, and
|
|
34
|
+
* one plugin holds exactly one config row (`plugin_configs.plugin_id` is a
|
|
35
|
+
* primary key). Absent means "whatever you are set up with", which is the
|
|
36
|
+
* ordinary case.
|
|
37
|
+
*
|
|
38
|
+
* ## Tools are declared here and executed by the host
|
|
39
|
+
*
|
|
40
|
+
* A request may carry {@link LlmToolDeclaration}s. What comes back is
|
|
41
|
+
* {@link LlmToolCall}s: data, not invocations. The host runs the tool and sends
|
|
42
|
+
* the result back as another message.
|
|
43
|
+
*
|
|
44
|
+
* That split is not ceremony. A callback crossing this boundary would be a
|
|
45
|
+
* function in a payload, and it would put station code inside a plugin, which is
|
|
46
|
+
* the wrong side of the fence for deciding what the station is allowed to do.
|
|
47
|
+
* Every shape in this file except {@link LlmHandle} is JSON-safe, and the one
|
|
48
|
+
* exception carries a stream on purpose.
|
|
49
|
+
*/
|
|
50
|
+
import type { PluginLifecycle } from '../plugin.lifecycle.js';
|
|
51
|
+
/**
|
|
52
|
+
* How hard a reasoning model should think before answering.
|
|
53
|
+
*
|
|
54
|
+
* Sent as `reasoning_effort`, and **only when the caller asked for it** — which is
|
|
55
|
+
* a hint, not a guarantee it reaches the server. A plugin may hold its own
|
|
56
|
+
* setting that overrides this, forwards it unchanged, or refuses to send it at
|
|
57
|
+
* all, and a plugin that has seen a 400 naming the field drops it for its own
|
|
58
|
+
* lifetime regardless of what a caller asks for afterward. Leave it unset for
|
|
59
|
+
* anything that is not a reasoning model: the field means nothing to a plain
|
|
60
|
+
* model and a strict OpenAI-compatible server answers 400 rather than ignoring
|
|
61
|
+
* it.
|
|
62
|
+
*/
|
|
63
|
+
export type LlmReasoningEffort = 'low' | 'medium' | 'high';
|
|
64
|
+
/** Why a generation stopped. `tool-calls` is the one the host's loop acts on. */
|
|
65
|
+
export type LlmFinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';
|
|
66
|
+
/** One turn of the conversation. */
|
|
67
|
+
export interface LlmMessage {
|
|
68
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
69
|
+
/**
|
|
70
|
+
* The words. Empty is legitimate on an `assistant` turn that did nothing but
|
|
71
|
+
* ask for a tool.
|
|
72
|
+
*/
|
|
73
|
+
content: string;
|
|
74
|
+
/**
|
|
75
|
+
* What this `assistant` turn asked for, when it asked for tools.
|
|
76
|
+
*
|
|
77
|
+
* The host replays it verbatim on the next call, because a model that cannot
|
|
78
|
+
* see its own tool call has no idea what the `tool` message after it is
|
|
79
|
+
* answering.
|
|
80
|
+
*/
|
|
81
|
+
toolCalls?: LlmToolCall[];
|
|
82
|
+
/** Which call this `tool` turn answers. Absent on every other role. */
|
|
83
|
+
toolCallId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* What the provider signed on this `assistant` turn, quoted back verbatim.
|
|
86
|
+
* Absent on every other role, and absent from a provider that signs nothing.
|
|
87
|
+
*
|
|
88
|
+
* Opaque to the host, which is the point: it is the plugin's own
|
|
89
|
+
* {@link LlmResult.providerState} handed straight back, so a provider that
|
|
90
|
+
* refuses a turn missing its own signature gets one that has it. See
|
|
91
|
+
* {@link LlmResult.providerState} for what puts it there.
|
|
92
|
+
*/
|
|
93
|
+
providerState?: Record<string, unknown>;
|
|
94
|
+
}
|
|
95
|
+
/** A tool the model may ask for. */
|
|
96
|
+
export interface LlmToolDeclaration {
|
|
97
|
+
/** How the model names it when calling. */
|
|
98
|
+
name: string;
|
|
99
|
+
/**
|
|
100
|
+
* What it does, written for the model rather than for a developer. This is
|
|
101
|
+
* the entire basis on which it decides whether to call the thing, so "current
|
|
102
|
+
* conditions and today's high and low for a place" beats "weather lookup".
|
|
103
|
+
*/
|
|
104
|
+
description: string;
|
|
105
|
+
/**
|
|
106
|
+
* JSON Schema for the arguments, as a plain object.
|
|
107
|
+
*
|
|
108
|
+
* Not a zod schema: this is a payload that is sent, and a schema instance is
|
|
109
|
+
* a class. Convert on the way in if that is what you hold.
|
|
110
|
+
*/
|
|
111
|
+
parameters: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
/** The model asking for a tool. Data, not an invocation. */
|
|
114
|
+
export interface LlmToolCall {
|
|
115
|
+
/** The model's own id for this call, quoted back on {@link LlmMessage.toolCallId}. */
|
|
116
|
+
id: string;
|
|
117
|
+
/** Which declaration it wants, by {@link LlmToolDeclaration.name}. */
|
|
118
|
+
name: string;
|
|
119
|
+
/**
|
|
120
|
+
* The arguments, already parsed out of the JSON the model produced.
|
|
121
|
+
*
|
|
122
|
+
* Unvalidated against the declared schema: the model is perfectly capable of
|
|
123
|
+
* inventing a field or omitting a required one, and the host checks before
|
|
124
|
+
* running anything.
|
|
125
|
+
*/
|
|
126
|
+
arguments: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
/** What one generation cost. Absent fields are ones the provider did not report. */
|
|
129
|
+
export interface LlmUsage {
|
|
130
|
+
inputTokens?: number;
|
|
131
|
+
outputTokens?: number;
|
|
132
|
+
totalTokens?: number;
|
|
133
|
+
/**
|
|
134
|
+
* How much of {@link outputTokens} went on thinking rather than on the answer.
|
|
135
|
+
*
|
|
136
|
+
* Separate because the two failures underneath one `outputTokens` figure are
|
|
137
|
+
* opposite: a model that wrote a long answer and one that spent its whole
|
|
138
|
+
* allowance reasoning and emitted nothing both finish on `length` at the same
|
|
139
|
+
* total, and only this tells them apart. A caller that logs a token count
|
|
140
|
+
* without it cannot answer "where did the allowance go" after the fact, which
|
|
141
|
+
* is the question a zero-pick run actually raises.
|
|
142
|
+
*
|
|
143
|
+
* Reported by the provider, so absent on plenty of them — see
|
|
144
|
+
* {@link reasoningChars} for what to fall back on.
|
|
145
|
+
*/
|
|
146
|
+
reasoningTokens?: number;
|
|
147
|
+
/**
|
|
148
|
+
* The length of the reasoning text, in characters.
|
|
149
|
+
*
|
|
150
|
+
* MEASURED rather than reported, and here for one reason: an
|
|
151
|
+
* OpenAI-compatible server that streams reasoning without counting it leaves
|
|
152
|
+
* {@link reasoningTokens} undefined, and a station running against one would
|
|
153
|
+
* otherwise learn nothing from either field. Characters are a poor unit and
|
|
154
|
+
* an honest one — roughly four to the token — so this answers the shape of
|
|
155
|
+
* the question ("all of it" versus "none of it") where the exact figure is
|
|
156
|
+
* not on offer.
|
|
157
|
+
*/
|
|
158
|
+
reasoningChars?: number;
|
|
159
|
+
}
|
|
160
|
+
/** One conversation to continue. */
|
|
161
|
+
export interface LlmRequest {
|
|
162
|
+
/**
|
|
163
|
+
* The conversation so far, oldest first, with the system prompt as the first
|
|
164
|
+
* turn where there is one.
|
|
165
|
+
*/
|
|
166
|
+
messages: LlmMessage[];
|
|
167
|
+
/** Which model, or absent for the plugin's configured one. */
|
|
168
|
+
model?: string;
|
|
169
|
+
/** Sampling temperature, or absent for the plugin's configured one. */
|
|
170
|
+
temperature?: number;
|
|
171
|
+
/** A ceiling on the answer, in tokens. Absent means the provider's own. */
|
|
172
|
+
maxOutputTokens?: number;
|
|
173
|
+
/** See {@link LlmReasoningEffort}. Absent means send nothing at all. */
|
|
174
|
+
reasoningEffort?: LlmReasoningEffort;
|
|
175
|
+
/**
|
|
176
|
+
* What the model may call.
|
|
177
|
+
*
|
|
178
|
+
* Only sent to a model that says it can (see {@link LlmModelInfo.tools}), so
|
|
179
|
+
* a plugin receiving this has already been told the model supports it. If it
|
|
180
|
+
* does not, answer `unsupported` rather than dropping them silently: a break
|
|
181
|
+
* written without the facts a tool would have supplied is worse than one that
|
|
182
|
+
* fell back to the deterministic writer.
|
|
183
|
+
*/
|
|
184
|
+
tools?: LlmToolDeclaration[];
|
|
185
|
+
}
|
|
186
|
+
/** Everything about a finished generation except the words as they arrived. */
|
|
187
|
+
export interface LlmResult {
|
|
188
|
+
/** The whole answer, accumulated. Empty when the model only asked for tools. */
|
|
189
|
+
text: string;
|
|
190
|
+
/** What it asked for. Empty on an ordinary answer. */
|
|
191
|
+
toolCalls: LlmToolCall[];
|
|
192
|
+
/** What it cost, where the provider said. */
|
|
193
|
+
usage?: LlmUsage;
|
|
194
|
+
finishReason: LlmFinishReason;
|
|
195
|
+
/**
|
|
196
|
+
* Whatever this provider SIGNED on this turn, in the plugin's own shape, for
|
|
197
|
+
* the host to hand back on the `assistant` message it builds out of this
|
|
198
|
+
* result. Absent when the provider signed nothing, which is every
|
|
199
|
+
* OpenAI-compatible server.
|
|
200
|
+
*
|
|
201
|
+
* The host never reads it. It exists because two providers refuse a tool
|
|
202
|
+
* round trip whose earlier turns arrive stripped: Anthropic will not accept a
|
|
203
|
+
* turn whose thinking block and its signature are missing, and Gemini wants
|
|
204
|
+
* its thought signatures back on the function calls it made. Both are facts
|
|
205
|
+
* about a wire protocol rather than about a conversation, so the station's
|
|
206
|
+
* boundary carries them without describing them.
|
|
207
|
+
*
|
|
208
|
+
* JSON-safe like everything else here: it is stored in a transcript and sent
|
|
209
|
+
* back across the boundary, so no class instances and no functions.
|
|
210
|
+
*/
|
|
211
|
+
providerState?: Record<string, unknown>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A generation in flight.
|
|
215
|
+
*
|
|
216
|
+
* Carries a live stream, which is why it is classified as a live-object type in
|
|
217
|
+
* `boundary.json.safe.ts` rather than as a payload. {@link LlmResult} is the
|
|
218
|
+
* payload, and it is JSON-safe.
|
|
219
|
+
*/
|
|
220
|
+
export interface LlmHandle {
|
|
221
|
+
/**
|
|
222
|
+
* The answer as it arrives.
|
|
223
|
+
*
|
|
224
|
+
* The host reads it to the end or cancels it, and either one releases what is
|
|
225
|
+
* underneath. **Cancelling has to actually stop the generation**, which is a
|
|
226
|
+
* requirement on the PLUGIN rather than something the platform can arrange:
|
|
227
|
+
* the host has no other handle on the work once `generate` has returned.
|
|
228
|
+
*
|
|
229
|
+
* The trap that makes this worth spelling out is that forwarding a provider's
|
|
230
|
+
* stream does NOT satisfy it by itself. A plugin that hands over one branch of
|
|
231
|
+
* a tee, or whose {@link result} keeps reading the source, has given the host
|
|
232
|
+
* a stream it can close and a generation it cannot stop — and the invocation
|
|
233
|
+
* signal is no help, because that is disposed the moment `generate` resolves
|
|
234
|
+
* and a generation legitimately outlives the call that started it. So a plugin
|
|
235
|
+
* owns an abort of its own, for as long as the words are still arriving.
|
|
236
|
+
*/
|
|
237
|
+
text: ReadableStream<string>;
|
|
238
|
+
/**
|
|
239
|
+
* Settles once the generation is done.
|
|
240
|
+
*
|
|
241
|
+
* **Read {@link text} first.** A provider stream that nobody is draining
|
|
242
|
+
* applies backpressure, so awaiting this without consuming the words is how a
|
|
243
|
+
* caller waits forever. {@link collectGeneration} exists so that ordering is
|
|
244
|
+
* not something each caller has to remember.
|
|
245
|
+
*/
|
|
246
|
+
result: Promise<LlmResult>;
|
|
247
|
+
}
|
|
248
|
+
/** One model this plugin can be asked for. */
|
|
249
|
+
export interface LlmModelInfo {
|
|
250
|
+
/** The id to pass back as {@link LlmRequest.model}. */
|
|
251
|
+
id: string;
|
|
252
|
+
/** What the console calls it. Absent means show the id. */
|
|
253
|
+
label?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Whether this model can be given {@link LlmRequest.tools}.
|
|
256
|
+
*
|
|
257
|
+
* Per MODEL, not per server, which is why it lives here rather than on the
|
|
258
|
+
* plugin: one endpoint commonly serves both a model that can call tools and
|
|
259
|
+
* one that cannot, and asking the wrong one is a failed generation rather
|
|
260
|
+
* than a degraded answer.
|
|
261
|
+
*/
|
|
262
|
+
tools: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Whether this is the model a request with no {@link LlmRequest.model} gets.
|
|
265
|
+
*
|
|
266
|
+
* Set it if you have one. Without it the host cannot tell which of the models
|
|
267
|
+
* you listed an unnamed request will actually reach, so it has to assume the
|
|
268
|
+
* worst one and will never send tools unless a caller names a model itself —
|
|
269
|
+
* which gets steadily more likely to be wrong the more models your server has.
|
|
270
|
+
*/
|
|
271
|
+
default?: boolean;
|
|
272
|
+
}
|
|
273
|
+
/** A plugin that can produce words. */
|
|
274
|
+
export interface LlmPluginInstance extends PluginLifecycle {
|
|
275
|
+
/**
|
|
276
|
+
* Continue the conversation.
|
|
277
|
+
*
|
|
278
|
+
* May return before any words exist, because the handle carries a stream and
|
|
279
|
+
* not the text, so a model that thinks for ten seconds shows up as a slow
|
|
280
|
+
* first chunk rather than as a slow `generate`.
|
|
281
|
+
*
|
|
282
|
+
* @throws {PluginError} `config` when the plugin is not set up enough to try
|
|
283
|
+
* (no server address, no model), `unsupported` when asked for tools the
|
|
284
|
+
* named model cannot do, `upstream` when the provider refused, `timeout`
|
|
285
|
+
* when it did not answer, `rate_limited` when it said to wait.
|
|
286
|
+
*/
|
|
287
|
+
generate(request: LlmRequest): Promise<LlmHandle>;
|
|
288
|
+
/**
|
|
289
|
+
* The models this plugin can be asked for, for a console drawing a list and
|
|
290
|
+
* for the host deciding whether it may send tools.
|
|
291
|
+
*
|
|
292
|
+
* Optional, like `listVoices` on the speech capability. But note what absent
|
|
293
|
+
* costs: the host has no way to learn that any model here supports tools, so
|
|
294
|
+
* it sends none. A plugin that wants tool calling has to describe itself.
|
|
295
|
+
*/
|
|
296
|
+
listModels?(): Promise<LlmModelInfo[]>;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Drain a handle and answer with the finished result.
|
|
300
|
+
*
|
|
301
|
+
* The ordinary way to use {@link LlmPluginInstance.generate} when the caller
|
|
302
|
+
* wants the answer rather than the words as they arrive. Streaming still happens
|
|
303
|
+
* underneath, so the host's slot is still released at the right moment; what this
|
|
304
|
+
* removes is the chance of awaiting {@link LlmHandle.result} without draining
|
|
305
|
+
* {@link LlmHandle.text} first.
|
|
306
|
+
*
|
|
307
|
+
* A free function rather than a method, following `jsonBody` and `tryJsonBody`:
|
|
308
|
+
* it keeps the handle the platform's own shape, and it is nothing a plugin should
|
|
309
|
+
* have to implement.
|
|
310
|
+
*
|
|
311
|
+
* Pass a `signal` to stop early. The drain is raced against it and the stream is
|
|
312
|
+
* cancelled, which — per {@link LlmHandle.text} — is what asks the plugin to stop
|
|
313
|
+
* generating. Without one this waits for the model however long it takes, which is
|
|
314
|
+
* right for a caller that has nothing better to do and wrong for one holding a
|
|
315
|
+
* slot something with a deadline is queued for.
|
|
316
|
+
*/
|
|
317
|
+
export declare function collectGeneration(handle: LlmHandle, signal?: AbortSignal): Promise<LlmResult>;
|
|
318
|
+
//# sourceMappingURL=llm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/capabilities/llm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `mixer` capability. A mixer plugin takes several pieces of audio and
|
|
3
|
+
* answers with one.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is not the analysis capability
|
|
6
|
+
*
|
|
7
|
+
* It was, for one commit, as an optional `joinAudio` method on
|
|
8
|
+
* {@link AnalysisProvider}. The argument was that joining and measuring are the
|
|
9
|
+
* same work seen from the other end: both need decoded PCM, which is the one
|
|
10
|
+
* thing this tree does not do in Node, so both want the same adapter over the
|
|
11
|
+
* same program. **That argument is about the ADAPTER and it is still true** —
|
|
12
|
+
* the bundled plugin declares both capabilities and serves them off one sidecar
|
|
13
|
+
* with one address.
|
|
14
|
+
*
|
|
15
|
+
* It is not an argument about the CONTRACT, because a capability here is the unit
|
|
16
|
+
* of SELECTION rather than the unit of implementation. The host picks one plugin
|
|
17
|
+
* per capability, so a joiner reached through the analysis pick is whichever
|
|
18
|
+
* plugin the operator chose to MEASURE with: install a second analyzer that
|
|
19
|
+
* measures better and cannot join, name it, and the station stops joining with
|
|
20
|
+
* one log line, and the only fix is to select a worse analyzer. Filtering the
|
|
21
|
+
* analysis candidates on the method instead would produce a second, disagreeing
|
|
22
|
+
* pick under one key, which is exactly the failure the host's plugin selection
|
|
23
|
+
* exists to prevent.
|
|
24
|
+
*
|
|
25
|
+
* So there are two keys. A station may measure with one engine and mix with
|
|
26
|
+
* another, and a plugin that only mixes — a filter-graph adapter with no detector
|
|
27
|
+
* in it — is a valid installation rather than an unexpressible one.
|
|
28
|
+
*
|
|
29
|
+
* ## The plugin is an adapter, not a mixer
|
|
30
|
+
*
|
|
31
|
+
* {@link AnalysisProvider}'s note applies here word for word and for the same
|
|
32
|
+
* reason: joining needs decoded PCM, and the expected implementation is a thin
|
|
33
|
+
* adapter over a separate program. Nothing here requires that shape.
|
|
34
|
+
*
|
|
35
|
+
* Every shape here is JSON-safe except {@link JoinedAudio}, which carries a live
|
|
36
|
+
* stream deliberately. Offsets and durations are integer milliseconds.
|
|
37
|
+
*/
|
|
38
|
+
import type { PluginLifecycle } from '../plugin.lifecycle.js';
|
|
39
|
+
/**
|
|
40
|
+
* Several pieces of audio to be made into one.
|
|
41
|
+
*
|
|
42
|
+
* The station's own use is a production: a phone-in or a podcast is written one
|
|
43
|
+
* beat at a time, because a beat is one model call in one voice, and joining the
|
|
44
|
+
* beats once they exist is what lets the programme air as ONE item with a pause
|
|
45
|
+
* between turns that somebody chose. See {@link MixerProvider.join}.
|
|
46
|
+
*/
|
|
47
|
+
export interface AudioJoin {
|
|
48
|
+
/**
|
|
49
|
+
* The parts, in the order they are to be heard.
|
|
50
|
+
*
|
|
51
|
+
* Each URL is complete and fetchable exactly as an analysis `audioUrl` is,
|
|
52
|
+
* and carries whatever authentication it needs. It has to be reachable from
|
|
53
|
+
* wherever the joining happens, which is not necessarily where this plugin
|
|
54
|
+
* runs.
|
|
55
|
+
*/
|
|
56
|
+
parts: Array<{
|
|
57
|
+
url: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* How much silence to put BETWEEN the parts, in milliseconds.
|
|
61
|
+
*
|
|
62
|
+
* Between, and never at the ends: what comes back is one item in a running
|
|
63
|
+
* order, and padding its head or tail is dead air at a boundary somebody
|
|
64
|
+
* else already trims.
|
|
65
|
+
*/
|
|
66
|
+
gapMs: number;
|
|
67
|
+
/**
|
|
68
|
+
* Take each part's own leading and trailing silence off before joining.
|
|
69
|
+
*
|
|
70
|
+
* Absent means yes, because it is what makes {@link gapMs} mean anything: a
|
|
71
|
+
* gap between two untrimmed parts is that gap plus two unknowns that move
|
|
72
|
+
* with the voice and with the line. Set it false only where the parts are
|
|
73
|
+
* known to be tight already and their exact lengths matter.
|
|
74
|
+
*/
|
|
75
|
+
trim?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Sounds mixed ON the joined parts rather than placed between them.
|
|
78
|
+
*
|
|
79
|
+
* Absent is the ordinary join, and every caller that sends none gets exactly
|
|
80
|
+
* what it got before this field existed.
|
|
81
|
+
*
|
|
82
|
+
* The difference from a part is that **nothing moves**. A part pushes
|
|
83
|
+
* everything after it later; an overlay happens at the same time as what is
|
|
84
|
+
* already there, so the words either side of a drop keep the timing they were
|
|
85
|
+
* spoken with. That is the difference between a presenter landing a joke and
|
|
86
|
+
* one waiting politely for their own sentence to finish.
|
|
87
|
+
*/
|
|
88
|
+
overlays?: AudioOverlay[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* One sound to mix onto a join.
|
|
92
|
+
*
|
|
93
|
+
* Anchored to a JOIN rather than to a timestamp, which is the one design decision
|
|
94
|
+
* here. A caller knows which boundary it means — after the setup, before the
|
|
95
|
+
* punchline — and does not know how long the parts will come out, because that is
|
|
96
|
+
* a fact about audio it has not decoded. Naming the boundary lets whatever IS
|
|
97
|
+
* decoding resolve it in samples.
|
|
98
|
+
*/
|
|
99
|
+
export interface AudioOverlay {
|
|
100
|
+
/** Complete and fetchable exactly as a part's url is. */
|
|
101
|
+
url: string;
|
|
102
|
+
/**
|
|
103
|
+
* Which join this sits at: `0` is the boundary after the first part.
|
|
104
|
+
*
|
|
105
|
+
* A join that does not exist is an ERROR rather than a nudge to the nearest
|
|
106
|
+
* one. An implementation that quietly moved it would put a sound somewhere
|
|
107
|
+
* nobody asked for and say nothing about having done so.
|
|
108
|
+
*/
|
|
109
|
+
afterIndex: number;
|
|
110
|
+
/**
|
|
111
|
+
* How far either side of that boundary to start it, in milliseconds.
|
|
112
|
+
*
|
|
113
|
+
* Zero is exactly on it. **Negative pulls the sound earlier**, under the tail
|
|
114
|
+
* of what came before, which is what this whole field exists for.
|
|
115
|
+
*/
|
|
116
|
+
offsetMs?: number;
|
|
117
|
+
/** What to do to the sound itself, in decibels. Absent leaves it alone. */
|
|
118
|
+
gainDb?: number;
|
|
119
|
+
/**
|
|
120
|
+
* How far to pull DOWN what is underneath, in decibels, for this overlay's
|
|
121
|
+
* span alone.
|
|
122
|
+
*
|
|
123
|
+
* The span rather than the whole join, because turning the speech down for a
|
|
124
|
+
* two-second drop is not the same request as turning the break down. Applied
|
|
125
|
+
* before the sum, or the duck would pull down the very sound it made room for.
|
|
126
|
+
*/
|
|
127
|
+
duckDb?: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The joined audio, and what it is.
|
|
131
|
+
*
|
|
132
|
+
* `SpeechHandle`'s shape, deliberately and for its reasons: `mime` is what the
|
|
133
|
+
* host stores and serves the bytes under, and the audio is a stream so a whole
|
|
134
|
+
* programme is never held in memory on either side of the call.
|
|
135
|
+
*/
|
|
136
|
+
export interface JoinedAudio {
|
|
137
|
+
/**
|
|
138
|
+
* What the bytes ARE, as a media type (`audio/flac`).
|
|
139
|
+
*
|
|
140
|
+
* Load-bearing rather than decoration, exactly as it is for speech: both
|
|
141
|
+
* consumers of station audio go by the header rather than by the bytes, so a
|
|
142
|
+
* flac announced as `audio/wav` fails as silence rather than as an error.
|
|
143
|
+
*/
|
|
144
|
+
mime: string;
|
|
145
|
+
/** The audio. The host reads it to the end or cancels it. */
|
|
146
|
+
audio: ReadableStream<Uint8Array>;
|
|
147
|
+
/**
|
|
148
|
+
* How long the result runs, if the joiner can say.
|
|
149
|
+
*
|
|
150
|
+
* Worth reporting because whatever just did the joining has already seen
|
|
151
|
+
* every sample, and a host that has to learn this some other way is a second
|
|
152
|
+
* decode of something it just received. Absent is fine.
|
|
153
|
+
*/
|
|
154
|
+
durationMs?: number;
|
|
155
|
+
}
|
|
156
|
+
/** A plugin that can make one piece of audio out of several. */
|
|
157
|
+
export interface MixerProvider extends PluginLifecycle {
|
|
158
|
+
/**
|
|
159
|
+
* Join several pieces of audio into one.
|
|
160
|
+
*
|
|
161
|
+
* **Required, where it was optional as `analyzeTrack`'s neighbour.** It was
|
|
162
|
+
* optional because it was a bolt-on to a capability that meant something
|
|
163
|
+
* else, and a plugin that declared `analysis` and could not join was the
|
|
164
|
+
* ordinary case. A plugin that declares THIS capability and cannot mix is not
|
|
165
|
+
* a state worth being able to express, so the host's "declared and
|
|
166
|
+
* implemented" check covers it like any other capability's one method.
|
|
167
|
+
*
|
|
168
|
+
* A station with no mixer at all is still an ordinary state rather than a
|
|
169
|
+
* fault: a production whose beats cannot be joined airs as a block of beats,
|
|
170
|
+
* which is what it did before anything could join them.
|
|
171
|
+
*
|
|
172
|
+
* Expect minutes rather than seconds — decoding several parts is not a
|
|
173
|
+
* request, it is a job — but honour `host.signal` all the same, because a
|
|
174
|
+
* station shutting down should not wait on a programme nobody will hear.
|
|
175
|
+
*
|
|
176
|
+
* @throws {PluginError} `config` when the plugin is not set up enough to try,
|
|
177
|
+
* `upstream` when a part could not be fetched or decoded, `unsupported`
|
|
178
|
+
* when whatever is behind this plugin cannot join what it was given,
|
|
179
|
+
* `timeout` when it did not answer.
|
|
180
|
+
*/
|
|
181
|
+
join(request: AudioJoin): Promise<JoinedAudio>;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=mixer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mixer.d.ts","sourceRoot":"","sources":["../../src/capabilities/mixer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB;;;;;;;OAOG;IACH,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE9B;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IACzB,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb,6DAA6D;IAC7D,KAAK,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAElC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAc,SAAQ,eAAe;IAClD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAClD"}
|