@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -0
  3. package/dist/article.parse.d.ts +82 -0
  4. package/dist/article.parse.d.ts.map +1 -0
  5. package/dist/boundary.json.safe.d.ts +191 -0
  6. package/dist/boundary.json.safe.d.ts.map +1 -0
  7. package/dist/capabilities/analysis.d.ts +330 -0
  8. package/dist/capabilities/analysis.d.ts.map +1 -0
  9. package/dist/capabilities/charts.d.ts +134 -0
  10. package/dist/capabilities/charts.d.ts.map +1 -0
  11. package/dist/capabilities/enrichment.d.ts +255 -0
  12. package/dist/capabilities/enrichment.d.ts.map +1 -0
  13. package/dist/capabilities/llm.d.ts +318 -0
  14. package/dist/capabilities/llm.d.ts.map +1 -0
  15. package/dist/capabilities/mixer.d.ts +183 -0
  16. package/dist/capabilities/mixer.d.ts.map +1 -0
  17. package/dist/capabilities/music.provider.d.ts +245 -0
  18. package/dist/capabilities/music.provider.d.ts.map +1 -0
  19. package/dist/capabilities/news.d.ts +171 -0
  20. package/dist/capabilities/news.d.ts.map +1 -0
  21. package/dist/capabilities/scrobble.d.ts +133 -0
  22. package/dist/capabilities/scrobble.d.ts.map +1 -0
  23. package/dist/capabilities/search.d.ts +122 -0
  24. package/dist/capabilities/search.d.ts.map +1 -0
  25. package/dist/capabilities/similarity.d.ts +101 -0
  26. package/dist/capabilities/similarity.d.ts.map +1 -0
  27. package/dist/capabilities/speech.d.ts +211 -0
  28. package/dist/capabilities/speech.d.ts.map +1 -0
  29. package/dist/capabilities/weather.d.ts +192 -0
  30. package/dist/capabilities/weather.d.ts.map +1 -0
  31. package/dist/chunk-7QVYU63E.js +7 -0
  32. package/dist/chunk-7QVYU63E.js.map +1 -0
  33. package/dist/define.plugin.d.ts +56 -0
  34. package/dist/define.plugin.d.ts.map +1 -0
  35. package/dist/feed.parse.d.ts +97 -0
  36. package/dist/feed.parse.d.ts.map +1 -0
  37. package/dist/html.text.d.ts +71 -0
  38. package/dist/html.text.d.ts.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1153 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/match.text.d.ts +28 -0
  44. package/dist/match.text.d.ts.map +1 -0
  45. package/dist/plugin.api.version.d.ts +9 -0
  46. package/dist/plugin.api.version.d.ts.map +1 -0
  47. package/dist/plugin.base.d.ts +80 -0
  48. package/dist/plugin.base.d.ts.map +1 -0
  49. package/dist/plugin.config.fields.d.ts +524 -0
  50. package/dist/plugin.config.fields.d.ts.map +1 -0
  51. package/dist/plugin.config.read.d.ts +52 -0
  52. package/dist/plugin.config.read.d.ts.map +1 -0
  53. package/dist/plugin.error.d.ts +177 -0
  54. package/dist/plugin.error.d.ts.map +1 -0
  55. package/dist/plugin.host.d.ts +220 -0
  56. package/dist/plugin.host.d.ts.map +1 -0
  57. package/dist/plugin.host.response.d.ts +42 -0
  58. package/dist/plugin.host.response.d.ts.map +1 -0
  59. package/dist/plugin.http.d.ts +80 -0
  60. package/dist/plugin.http.d.ts.map +1 -0
  61. package/dist/plugin.lifecycle.d.ts +68 -0
  62. package/dist/plugin.lifecycle.d.ts.map +1 -0
  63. package/dist/plugin.manifest.d.ts +282 -0
  64. package/dist/plugin.manifest.d.ts.map +1 -0
  65. package/dist/plugin.permissions.d.ts +186 -0
  66. package/dist/plugin.permissions.d.ts.map +1 -0
  67. package/dist/testing/fake.plugin.host.d.ts +85 -0
  68. package/dist/testing/fake.plugin.host.d.ts.map +1 -0
  69. package/dist/testing/index.d.ts +10 -0
  70. package/dist/testing/index.d.ts.map +1 -0
  71. package/dist/testing/index.js +141 -0
  72. package/dist/testing/index.js.map +1 -0
  73. 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"}