@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,82 @@
|
|
|
1
|
+
import type { HostFetchInit, PluginHost } from './plugin.host.js';
|
|
2
|
+
/**
|
|
3
|
+
* The story behind a headline, as the paragraphs somebody else published.
|
|
4
|
+
*
|
|
5
|
+
* The sibling of `feed.parse.ts`, here for the same reason and with the same
|
|
6
|
+
* split. A feed is a summary of a publication and frequently not a summary of
|
|
7
|
+
* anything else: measured against the station's own configured feed, every
|
|
8
|
+
* item's `description` was one sentence restating its title and its
|
|
9
|
+
* `content:encoded` was that same sentence wrapped in a `<p>`. So a station
|
|
10
|
+
* that wants to say what HAPPENED has to read the page the entry points at,
|
|
11
|
+
* and every plugin that reads a feed will want this the moment it wants more
|
|
12
|
+
* than titles.
|
|
13
|
+
*
|
|
14
|
+
* ## It extracts and it does not summarise
|
|
15
|
+
*
|
|
16
|
+
* Everything that comes out of here is the publisher's own prose, in the
|
|
17
|
+
* publisher's own order, with the furniture removed. Nothing is rewritten,
|
|
18
|
+
* nothing is joined, and no sentence is composed. That is the same boundary
|
|
19
|
+
* `plugins/wikipedia` keeps by handing the host an article verbatim: only the
|
|
20
|
+
* host can check a claim against the text it came from, and prose that has been
|
|
21
|
+
* through a plugin's own paraphrase is prose nothing can check.
|
|
22
|
+
*
|
|
23
|
+
* The one thing removed that is not furniture is a paragraph announcing itself as an advertisement
|
|
24
|
+
* — see {@link SPONSOR_OPENERS}. It is still a cut rather than a rewrite: what is left is the
|
|
25
|
+
* publisher's own prose, and what went is prose the publisher was paid for.
|
|
26
|
+
*
|
|
27
|
+
* ## Wrong in one direction on purpose
|
|
28
|
+
*
|
|
29
|
+
* A page that cannot be read answers `undefined` rather than a guess. Every
|
|
30
|
+
* caller's fallback is the entry's own words, which is a real answer, whereas
|
|
31
|
+
* navigation copy and cookie banners scraped off a template are noise a voice
|
|
32
|
+
* would read out as news. So the extraction is deliberately conservative: a
|
|
33
|
+
* block that is not clearly a paragraph of prose is dropped, and a page that
|
|
34
|
+
* yields nothing is a page with nothing on it as far as anything here is
|
|
35
|
+
* concerned.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* How much of an article is kept.
|
|
39
|
+
*
|
|
40
|
+
* Sized to what a bulletin can use rather than to the article: the destination
|
|
41
|
+
* is a model's context beside a persona sheet and a set of content rules, and a
|
|
42
|
+
* writer that only ever reads out one sentence per story does not need three
|
|
43
|
+
* thousand words to find it. Cut on a sentence boundary
|
|
44
|
+
* ({@link truncateSentences}), because half a sentence is something a model
|
|
45
|
+
* finishes out of its own head.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ARTICLE_MAX_CHARS = 2000;
|
|
48
|
+
/**
|
|
49
|
+
* An article page as plain text, or `undefined` when it does not carry one.
|
|
50
|
+
*
|
|
51
|
+
* Pure, and that is what makes it testable against saved pages with no host in
|
|
52
|
+
* the way — `parseFeed`'s split, for `parseFeed`'s reason.
|
|
53
|
+
*
|
|
54
|
+
* @param html - The page, as served.
|
|
55
|
+
* @param maxChars - Where to cut. See {@link ARTICLE_MAX_CHARS}.
|
|
56
|
+
*/
|
|
57
|
+
export declare function extractArticle(html: string, maxChars?: number): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* An article off the network, as plain text.
|
|
60
|
+
*
|
|
61
|
+
* Everything about the request except the status check is `host.fetch`'s, and
|
|
62
|
+
* the status ladder is `plugin.http.ts`'s unmodified: `fetchFeed`'s shape, for
|
|
63
|
+
* `fetchFeed`'s reasons.
|
|
64
|
+
*
|
|
65
|
+
* The one thing this adds is the content-type check, which is not fussiness. An
|
|
66
|
+
* entry legitimately links to a PDF, an audio file or a video page, and a
|
|
67
|
+
* megabyte of binary put through a tag stripper produces a long string of
|
|
68
|
+
* plausible-looking rubbish rather than an error — which a bulletin would then
|
|
69
|
+
* read out.
|
|
70
|
+
*
|
|
71
|
+
* `options.maxChars` is separate from `init` because the two are addressed to
|
|
72
|
+
* different parties: everything in `init` is the host's business and this is the
|
|
73
|
+
* parser's. The default is {@link ARTICLE_MAX_CHARS}, which is sized for a
|
|
74
|
+
* bulletin — a writer that reads one sentence per story out of a model's context
|
|
75
|
+
* has no use for three thousand words. A plugin contributing an enrichment
|
|
76
|
+
* `SourceDocument` wants far more of the page, because what reads that is a
|
|
77
|
+
* claim extractor rather than a presenter.
|
|
78
|
+
*/
|
|
79
|
+
export declare function fetchArticle(host: PluginHost, url: string, init?: HostFetchInit, options?: {
|
|
80
|
+
maxChars?: number;
|
|
81
|
+
}): Promise<string | undefined>;
|
|
82
|
+
//# sourceMappingURL=article.parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"article.parse.d.ts","sourceRoot":"","sources":["../src/article.parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AAwIvC;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAA0B,GAAG,MAAM,GAAG,SAAS,CAgCrG;AA2BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CAC9B,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0B7B"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time enforcement of the JSON-safe rule, over the payloads it applies
|
|
3
|
+
* to.
|
|
4
|
+
*
|
|
5
|
+
* ## Which payloads, and why
|
|
6
|
+
*
|
|
7
|
+
* This rule used to cover everything a plugin touched, on the strength of a deferred move behind a
|
|
8
|
+
* subprocess: JSON was going to be the wire format, so nothing could carry a `Date`, a `Uint8Array`
|
|
9
|
+
* or a live object. That move is closed (`packages/plugin-sdk/CLAUDE.md` § "Trust and egress"), and
|
|
10
|
+
* with it the reason to apply the rule to `host.fetch`'s arguments and return value, which nothing
|
|
11
|
+
* serializes.
|
|
12
|
+
*
|
|
13
|
+
* What is registered here is what has an INDEPENDENT reason to survive
|
|
14
|
+
* `JSON.parse(JSON.stringify(x))`: it is stored in Postgres, or sent to the
|
|
15
|
+
* console over HTTP, or both. A `Date` in `TrackEnrichment` is a bug whether or
|
|
16
|
+
* not plugins ever move anywhere, which is why that half of the rule outlived
|
|
17
|
+
* the argument that introduced it.
|
|
18
|
+
*
|
|
19
|
+
* ## Why compile time
|
|
20
|
+
*
|
|
21
|
+
* The runtime conformance suite in `tests/` round-trips hand-written fixtures,
|
|
22
|
+
* which catches a value that violates the rule despite a type that permits it
|
|
23
|
+
* (a class instance where the type says `object`). What it cannot catch is a
|
|
24
|
+
* newly added field: nothing populates it, so nothing round-trips it, and CI
|
|
25
|
+
* stays green while a `Date` crosses the boundary.
|
|
26
|
+
*
|
|
27
|
+
* TypeScript types are erased, so no runtime walk can ever be exhaustive over
|
|
28
|
+
* a type. That check has to happen at compile time, which is what this file
|
|
29
|
+
* is. {@link AssertAllBoundaryPayloadsAreJsonSafe} fails `tsc` the moment any
|
|
30
|
+
* field of any registered payload stops being JSON-safe, without anyone
|
|
31
|
+
* writing a fixture for it.
|
|
32
|
+
*
|
|
33
|
+
* This lives in `src/` rather than `tests/` on purpose: per the repo
|
|
34
|
+
* convention the build tsconfig includes only `./src/**\/*`, so an assertion
|
|
35
|
+
* in `tests/` would never run during `tsc --noEmit`. Everything here is types
|
|
36
|
+
* plus three name arrays, so the runtime cost is the arrays alone.
|
|
37
|
+
*/
|
|
38
|
+
import type { AnalysisRef, TrackAnalysis, TrackCuePoints, TrackLoudness, TrackTaggedLoudness } from './capabilities/analysis.js';
|
|
39
|
+
import type { AudioJoin, AudioOverlay } from './capabilities/mixer.js';
|
|
40
|
+
import type { ChartDescriptor, ChartEntry, ChartQuery } from './capabilities/charts.js';
|
|
41
|
+
import type { NewsFeedDescriptor, NewsItem, NewsQuery } from './capabilities/news.js';
|
|
42
|
+
import type { ScrobblePlay, ScrobbleRejection, ScrobbleResult } from './capabilities/scrobble.js';
|
|
43
|
+
import type { SearchQuery, SearchResult } from './capabilities/search.js';
|
|
44
|
+
import type { ArtistTrack, SimilarArtist } from './capabilities/similarity.js';
|
|
45
|
+
import type { WeatherConditions, WeatherDay, WeatherQuery, WeatherReading } from './capabilities/weather.js';
|
|
46
|
+
import type { AlbumEnrichment, AlbumRef, ArtistEnrichment, ArtistRef, ExternalId, ExternalLink, SourceDocument, TrackEnrichment, TrackRef } from './capabilities/enrichment.js';
|
|
47
|
+
import type { GetPlaylistTracksOptions, ListPlaylistsOptions, PlaybackState, ProviderPlaylist, ProviderStream, ProviderTrack, SearchTracksOptions } from './capabilities/music.provider.js';
|
|
48
|
+
import type { LlmMessage, LlmModelInfo, LlmRequest, LlmResult, LlmToolCall, LlmToolDeclaration, LlmUsage } from './capabilities/llm.js';
|
|
49
|
+
import type { SpeechRequest, SpeechVoice } from './capabilities/speech.js';
|
|
50
|
+
import type { ConfigField, ConfigFieldColumn, ConfigFieldOption } from './plugin.config.fields.js';
|
|
51
|
+
import type { TrackFetchRequest, TrackFetchSession } from './plugin.host.js';
|
|
52
|
+
import type { PluginConnectionResult } from './plugin.lifecycle.js';
|
|
53
|
+
import type { PluginManifest } from './plugin.manifest.js';
|
|
54
|
+
import type { NetworkPermissionFromConfig, NetworkPermissionHost, PluginGrantRequest, PluginPermissions } from './plugin.permissions.js';
|
|
55
|
+
/**
|
|
56
|
+
* `T` with every part that cannot survive `JSON.parse(JSON.stringify(x))`
|
|
57
|
+
* replaced by `never`, so `T extends JsonSafe<T>` holds only for a genuinely
|
|
58
|
+
* JSON-safe `T`.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately narrower than structured-clone-safe. `Date`, `Map`, `Set` and
|
|
61
|
+
* typed arrays all survive `structuredClone` and are still rejected here,
|
|
62
|
+
* because what these payloads actually have to survive is Postgres and HTTP,
|
|
63
|
+
* and both of those are JSON. A `Date` that round-trips through
|
|
64
|
+
* `structuredClone` still comes back out of a `jsonb` column as a string.
|
|
65
|
+
*
|
|
66
|
+
* `any` and `unknown` pass through unchecked: there is nothing to inspect at
|
|
67
|
+
* compile time, which is exactly the case the runtime fixture round-trip
|
|
68
|
+
* covers.
|
|
69
|
+
*/
|
|
70
|
+
export type JsonSafe<T> = 0 extends 1 & T ? T : unknown extends T ? T : T extends string | number | boolean | null | undefined ? T : T extends (...args: never[]) => unknown ? never : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> ? never : T extends ArrayBuffer | SharedArrayBuffer | ArrayBufferView ? never : T extends bigint | symbol ? never : T extends readonly (infer TElement)[] ? readonly JsonSafe<TElement>[] : T extends object ? {
|
|
71
|
+
[K in keyof T]: JsonSafe<T[K]>;
|
|
72
|
+
} : never;
|
|
73
|
+
/**
|
|
74
|
+
* `true` when `T` survives the boundary, `false` when any part of it does not.
|
|
75
|
+
*
|
|
76
|
+
* The tuple wrappers stop the conditional from distributing over a union, so a
|
|
77
|
+
* field typed `string | Date` fails as a whole rather than partially matching.
|
|
78
|
+
*
|
|
79
|
+
* Written as a conditional rather than the more obvious
|
|
80
|
+
* `T extends JsonSafe<T>` constraint, because TypeScript rejects that form as
|
|
81
|
+
* a circular constraint (TS2313).
|
|
82
|
+
*/
|
|
83
|
+
type IsJsonSafe<T> = [T] extends [JsonSafe<T>] ? true : false;
|
|
84
|
+
/**
|
|
85
|
+
* Compiles only when every value in `T` is `true`. When one is not, the error
|
|
86
|
+
* lands on that property, so the diagnostic names the offending boundary type.
|
|
87
|
+
*/
|
|
88
|
+
type AssertAllTrue<T extends Record<string, true>> = T;
|
|
89
|
+
/**
|
|
90
|
+
* Every payload that is stored or sent, asserted in one place.
|
|
91
|
+
*
|
|
92
|
+
* ADDING A BOUNDARY TYPE? Add it here and to {@link JSON_SAFE_PAYLOAD_TYPES}.
|
|
93
|
+
* `tests/boundary.registry.test.ts` fails if an exported interface in a
|
|
94
|
+
* boundary source file is in none of the three registries, so this cannot be
|
|
95
|
+
* skipped by accident.
|
|
96
|
+
*
|
|
97
|
+
* `PluginManifest` is asserted without `configSchema`, which is a zod schema:
|
|
98
|
+
* a class instance, deliberately never serialized, and stripped by the host
|
|
99
|
+
* before a manifest is sent anywhere.
|
|
100
|
+
*/
|
|
101
|
+
export type AssertAllBoundaryPayloadsAreJsonSafe = AssertAllTrue<{
|
|
102
|
+
PluginConnectionResult: IsJsonSafe<PluginConnectionResult>;
|
|
103
|
+
PluginPermissions: IsJsonSafe<PluginPermissions>;
|
|
104
|
+
NetworkPermissionHost: IsJsonSafe<NetworkPermissionHost>;
|
|
105
|
+
NetworkPermissionFromConfig: IsJsonSafe<NetworkPermissionFromConfig>;
|
|
106
|
+
PluginGrantRequest: IsJsonSafe<PluginGrantRequest>;
|
|
107
|
+
ConfigField: IsJsonSafe<ConfigField>;
|
|
108
|
+
ConfigFieldOption: IsJsonSafe<ConfigFieldOption>;
|
|
109
|
+
ConfigFieldColumn: IsJsonSafe<ConfigFieldColumn>;
|
|
110
|
+
PluginManifestWithoutConfigSchema: IsJsonSafe<Omit<PluginManifest, 'configSchema'>>;
|
|
111
|
+
ProviderTrack: IsJsonSafe<ProviderTrack>;
|
|
112
|
+
ProviderPlaylist: IsJsonSafe<ProviderPlaylist>;
|
|
113
|
+
ProviderStream: IsJsonSafe<ProviderStream>;
|
|
114
|
+
TrackFetchSession: IsJsonSafe<TrackFetchSession>;
|
|
115
|
+
TrackFetchRequest: IsJsonSafe<TrackFetchRequest>;
|
|
116
|
+
SearchTracksOptions: IsJsonSafe<SearchTracksOptions>;
|
|
117
|
+
ListPlaylistsOptions: IsJsonSafe<ListPlaylistsOptions>;
|
|
118
|
+
GetPlaylistTracksOptions: IsJsonSafe<GetPlaylistTracksOptions>;
|
|
119
|
+
PlaybackState: IsJsonSafe<PlaybackState>;
|
|
120
|
+
TrackRef: IsJsonSafe<TrackRef>;
|
|
121
|
+
ArtistRef: IsJsonSafe<ArtistRef>;
|
|
122
|
+
AlbumRef: IsJsonSafe<AlbumRef>;
|
|
123
|
+
ExternalId: IsJsonSafe<ExternalId>;
|
|
124
|
+
ExternalLink: IsJsonSafe<ExternalLink>;
|
|
125
|
+
SourceDocument: IsJsonSafe<SourceDocument>;
|
|
126
|
+
TrackEnrichment: IsJsonSafe<TrackEnrichment>;
|
|
127
|
+
ArtistEnrichment: IsJsonSafe<ArtistEnrichment>;
|
|
128
|
+
AlbumEnrichment: IsJsonSafe<AlbumEnrichment>;
|
|
129
|
+
SpeechRequest: IsJsonSafe<SpeechRequest>;
|
|
130
|
+
SpeechVoice: IsJsonSafe<SpeechVoice>;
|
|
131
|
+
LlmMessage: IsJsonSafe<LlmMessage>;
|
|
132
|
+
LlmToolDeclaration: IsJsonSafe<LlmToolDeclaration>;
|
|
133
|
+
LlmToolCall: IsJsonSafe<LlmToolCall>;
|
|
134
|
+
LlmUsage: IsJsonSafe<LlmUsage>;
|
|
135
|
+
LlmRequest: IsJsonSafe<LlmRequest>;
|
|
136
|
+
LlmResult: IsJsonSafe<LlmResult>;
|
|
137
|
+
LlmModelInfo: IsJsonSafe<LlmModelInfo>;
|
|
138
|
+
AnalysisRef: IsJsonSafe<AnalysisRef>;
|
|
139
|
+
AudioJoin: IsJsonSafe<AudioJoin>;
|
|
140
|
+
AudioOverlay: IsJsonSafe<AudioOverlay>;
|
|
141
|
+
TrackCuePoints: IsJsonSafe<TrackCuePoints>;
|
|
142
|
+
TrackLoudness: IsJsonSafe<TrackLoudness>;
|
|
143
|
+
TrackTaggedLoudness: IsJsonSafe<TrackTaggedLoudness>;
|
|
144
|
+
TrackAnalysis: IsJsonSafe<TrackAnalysis>;
|
|
145
|
+
ChartDescriptor: IsJsonSafe<ChartDescriptor>;
|
|
146
|
+
ChartQuery: IsJsonSafe<ChartQuery>;
|
|
147
|
+
ChartEntry: IsJsonSafe<ChartEntry>;
|
|
148
|
+
NewsFeedDescriptor: IsJsonSafe<NewsFeedDescriptor>;
|
|
149
|
+
NewsQuery: IsJsonSafe<NewsQuery>;
|
|
150
|
+
NewsItem: IsJsonSafe<NewsItem>;
|
|
151
|
+
SimilarArtist: IsJsonSafe<SimilarArtist>;
|
|
152
|
+
ArtistTrack: IsJsonSafe<ArtistTrack>;
|
|
153
|
+
SearchQuery: IsJsonSafe<SearchQuery>;
|
|
154
|
+
SearchResult: IsJsonSafe<SearchResult>;
|
|
155
|
+
WeatherQuery: IsJsonSafe<WeatherQuery>;
|
|
156
|
+
WeatherConditions: IsJsonSafe<WeatherConditions>;
|
|
157
|
+
WeatherDay: IsJsonSafe<WeatherDay>;
|
|
158
|
+
WeatherReading: IsJsonSafe<WeatherReading>;
|
|
159
|
+
ScrobblePlay: IsJsonSafe<ScrobblePlay>;
|
|
160
|
+
ScrobbleRejection: IsJsonSafe<ScrobbleRejection>;
|
|
161
|
+
ScrobbleResult: IsJsonSafe<ScrobbleResult>;
|
|
162
|
+
}>;
|
|
163
|
+
/**
|
|
164
|
+
* Names of the interfaces asserted above. Kept as a runtime array so the
|
|
165
|
+
* registry-coverage test can compare it against what is actually exported
|
|
166
|
+
* from the boundary source files.
|
|
167
|
+
*/
|
|
168
|
+
export declare const JSON_SAFE_PAYLOAD_TYPES: readonly ["PluginConnectionResult", "PluginPermissions", "NetworkPermissionHost", "NetworkPermissionFromConfig", "PluginGrantRequest", "ConfigField", "ConfigFieldOption", "ConfigFieldColumn", "PluginManifest", "ProviderTrack", "ProviderPlaylist", "ProviderStream", "TrackFetchSession", "TrackFetchRequest", "SearchTracksOptions", "ListPlaylistsOptions", "GetPlaylistTracksOptions", "PlaybackState", "TrackRef", "ArtistRef", "AlbumRef", "ExternalId", "ExternalLink", "SourceDocument", "TrackEnrichment", "ArtistEnrichment", "AlbumEnrichment", "SpeechRequest", "SpeechVoice", "LlmMessage", "LlmToolDeclaration", "LlmToolCall", "LlmUsage", "LlmRequest", "LlmResult", "LlmModelInfo", "AnalysisRef", "AudioJoin", "AudioOverlay", "TrackCuePoints", "TrackLoudness", "TrackTaggedLoudness", "TrackAnalysis", "ChartDescriptor", "ChartQuery", "ChartEntry", "NewsFeedDescriptor", "NewsQuery", "NewsItem", "SimilarArtist", "ArtistTrack", "SearchQuery", "SearchResult", "WeatherQuery", "WeatherConditions", "WeatherDay", "WeatherReading", "ScrobblePlay", "ScrobbleRejection", "ScrobbleResult"];
|
|
169
|
+
/**
|
|
170
|
+
* Boundary interfaces that are deliberately NOT payloads: they describe
|
|
171
|
+
* methods, so they carry functions by definition and can never be JSON-safe.
|
|
172
|
+
*
|
|
173
|
+
* The arguments and return values of those methods are payloads, and those
|
|
174
|
+
* are what {@link JSON_SAFE_PAYLOAD_TYPES} covers. Listing the method-bearing
|
|
175
|
+
* interfaces explicitly is what makes "is this a payload or a contract?" a
|
|
176
|
+
* conscious decision for anyone adding one, rather than a silent omission.
|
|
177
|
+
*/
|
|
178
|
+
export declare const BOUNDARY_METHOD_TYPES: readonly ["PluginLogger", "PluginStorage", "PluginSecrets", "PluginConfigAccess", "PluginOAuth", "PluginEvents", "PluginTrackFetcher", "PluginHost", "PluginLifecycle", "MusicProviderCatalog", "MusicProviderStream", "MusicProviderSteer", "MusicProviderOAuth", "MusicProvider", "EnrichmentProvider", "SpeechPluginInstance", "LlmPluginInstance", "AnalysisProvider", "MixerProvider", "ChartsProvider", "NewsProvider", "SimilarityProvider", "SearchProvider", "WeatherProvider", "ScrobbleProvider"];
|
|
179
|
+
/**
|
|
180
|
+
* Boundary interfaces that deliberately carry a LIVE object, and so are neither
|
|
181
|
+
* payloads nor method contracts.
|
|
182
|
+
*
|
|
183
|
+
* The host and the plugin share a realm, permanently (see `packages/plugin-sdk/CLAUDE.md` § "Trust
|
|
184
|
+
* and egress"), so handing over a real `AbortSignal` or a real stream is the correct design rather
|
|
185
|
+
* than a shortcut around the rule. They are listed rather than simply left out, because the
|
|
186
|
+
* registry-coverage test treats an unclassified boundary interface as an omission, and "this one
|
|
187
|
+
* holds a live object on purpose" is a decision somebody should have to make in writing.
|
|
188
|
+
*/
|
|
189
|
+
export declare const BOUNDARY_LIVE_OBJECT_TYPES: readonly ["HostFetchInit", "SpeechHandle", "JoinedAudio", "LlmHandle"];
|
|
190
|
+
export {};
|
|
191
|
+
//# sourceMappingURL=boundary.json.safe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary.json.safe.d.ts","sourceRoot":"","sources":["../src/boundary.json.safe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `analysis` capability. An analysis plugin takes one track's audio and
|
|
3
|
+
* answers with measurements of it: where the record actually starts, where it is
|
|
4
|
+
* underway, where the ending begins, where it stops.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is not enrichment
|
|
7
|
+
*
|
|
8
|
+
* Enrichment asks an upstream what it KNOWS about a recording, fans the question
|
|
9
|
+
* out to everything that answers, and merges the results in priority order.
|
|
10
|
+
* Nothing here works that way. There is no upstream that knows where a record's
|
|
11
|
+
* outro begins — it is computed from the samples — so there is exactly one
|
|
12
|
+
* answer, one source, and nothing to merge. `TrackRef` also carries no way to
|
|
13
|
+
* reach the audio, deliberately, because no enrichment source wants it.
|
|
14
|
+
*
|
|
15
|
+
* ## The plugin is an adapter, not an analyzer
|
|
16
|
+
*
|
|
17
|
+
* Measuring any of this needs decoded PCM, and decoding is the one thing this
|
|
18
|
+
* tree does not do in Node. So the expected implementation is a thin adapter over
|
|
19
|
+
* a separate program: take {@link AnalysisRef.audioUrl}, hand it to whatever
|
|
20
|
+
* actually decodes, return what comes back. That is the same relationship the
|
|
21
|
+
* bundled speech plugin has with its engine.
|
|
22
|
+
*
|
|
23
|
+
* The reason is decoding and only decoding. An earlier version of this note also claimed the
|
|
24
|
+
* boundary bought a licence position, which `analysis/README.md` § "The rule, stated once" retired:
|
|
25
|
+
* nothing copyleft or non-commercial enters the analysis path in the first place, so there is no
|
|
26
|
+
* position for the boundary to buy.
|
|
27
|
+
*
|
|
28
|
+
* Nothing here requires that shape. A plugin that can measure audio some other
|
|
29
|
+
* way is a valid implementation; this note exists so the first one is not
|
|
30
|
+
* mistaken for the contract.
|
|
31
|
+
*
|
|
32
|
+
* ## Why joining audio is not here
|
|
33
|
+
*
|
|
34
|
+
* It was, for one commit, as an optional `joinAudio`. The argument was the
|
|
35
|
+
* paragraph above read backwards — joining needs decoded PCM too, so it wants the
|
|
36
|
+
* same adapter over the same program, and the bundled plugin does serve both off
|
|
37
|
+
* one sidecar with one address. That much is still true and is why there is still
|
|
38
|
+
* one plugin.
|
|
39
|
+
*
|
|
40
|
+
* What it got wrong is that a capability is the unit of SELECTION, not of
|
|
41
|
+
* implementation: the host picks one plugin per capability, so a joiner carried
|
|
42
|
+
* here is whichever plugin the operator chose to MEASURE with. `capabilities/mixer.ts`
|
|
43
|
+
* has the rest of it.
|
|
44
|
+
*
|
|
45
|
+
* ## What the host guarantees, and what it cannot
|
|
46
|
+
*
|
|
47
|
+
* The host resolves {@link AnalysisRef.audioUrl} because a plugin cannot: the
|
|
48
|
+
* copy that can actually be served is a binding the catalog owns, and asking one
|
|
49
|
+
* plugin to go through another is not a thing the host permits. In exchange the
|
|
50
|
+
* host cannot see the bytes, so it cannot tell a complete download from a
|
|
51
|
+
* truncated one — which is why {@link TrackAnalysis.complete} is reported rather
|
|
52
|
+
* than inferred. See the note on that field: it is the difference between a cache
|
|
53
|
+
* that can be trusted and one that cannot.
|
|
54
|
+
*
|
|
55
|
+
* Every shape here is JSON-safe. Offsets are integer milliseconds.
|
|
56
|
+
*/
|
|
57
|
+
import type { PluginLifecycle } from '../plugin.lifecycle.js';
|
|
58
|
+
/**
|
|
59
|
+
* The current shape of {@link TrackAnalysis.data}.
|
|
60
|
+
*
|
|
61
|
+
* Bumped whenever a detector's output changes shape, which is what lets a stored
|
|
62
|
+
* row be recognised as STALE rather than read as missing or, worse, as current.
|
|
63
|
+
* Reanalysis then falls out of an ordinary "needs work" query instead of needing
|
|
64
|
+
* a migration.
|
|
65
|
+
*
|
|
66
|
+
* **An OPTIONAL field being added is not that**, and does not bump this. Every
|
|
67
|
+
* consumer of `data` already has a defined answer for a field that is absent —
|
|
68
|
+
* it has to, since an analyzer may not compute one — so a row written before the
|
|
69
|
+
* field existed is still a correct row of this version rather than a stale one.
|
|
70
|
+
* Bumping for it would mark the whole catalog for re-measurement to gain
|
|
71
|
+
* something the station degrades over anyway. `tagGainDb` and friends arrived
|
|
72
|
+
* exactly this way.
|
|
73
|
+
*
|
|
74
|
+
* The host compares this against what it stored, so a plugin must report the
|
|
75
|
+
* version it actually produced rather than this constant, in case the two have
|
|
76
|
+
* drifted apart across an upgrade.
|
|
77
|
+
*/
|
|
78
|
+
export declare const ANALYSIS_SCHEMA_VERSION = 1;
|
|
79
|
+
/**
|
|
80
|
+
* How the host asks about one track.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately not a {@link TrackRef}: nothing here is matched by name, so
|
|
83
|
+
* artist and title would be decoration. What a measurement needs is bytes and a
|
|
84
|
+
* way to tell a truncation from a short record.
|
|
85
|
+
*/
|
|
86
|
+
export interface AnalysisRef {
|
|
87
|
+
/**
|
|
88
|
+
* The canonical catalog id for this track.
|
|
89
|
+
*
|
|
90
|
+
* Passed for the plugin's own logging and for its own caching, if it keeps
|
|
91
|
+
* any. It is not a key into anything the plugin can read, and the host does
|
|
92
|
+
* not expect it back.
|
|
93
|
+
*/
|
|
94
|
+
trackId: string;
|
|
95
|
+
/**
|
|
96
|
+
* A complete, fetchable URL for the audio, resolved by the host.
|
|
97
|
+
*
|
|
98
|
+
* Carries its own authentication, exactly as the playout URLs do: whatever
|
|
99
|
+
* fetches this sends no headers on the host's behalf. It may be short-lived,
|
|
100
|
+
* so fetch it during the call rather than storing it.
|
|
101
|
+
*
|
|
102
|
+
* **It has to be reachable from wherever the decoding happens**, which is not
|
|
103
|
+
* necessarily where this plugin runs. An address that resolves inside the
|
|
104
|
+
* API process and not inside a sidecar container is the first thing to check
|
|
105
|
+
* when every analysis fails at once.
|
|
106
|
+
*/
|
|
107
|
+
audioUrl: string;
|
|
108
|
+
/**
|
|
109
|
+
* How long the catalog believes the track is.
|
|
110
|
+
*
|
|
111
|
+
* A cross-check rather than an input to any measurement: audio that decodes
|
|
112
|
+
* to appreciably less than this was truncated, and measuring it would report
|
|
113
|
+
* a confident cold ending for a record that fades. Absent when the catalog
|
|
114
|
+
* never learned a duration, which is ordinary and is not a reason to refuse.
|
|
115
|
+
*/
|
|
116
|
+
durationMs?: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The four points, all absolute offsets into the file.
|
|
120
|
+
*
|
|
121
|
+
* Including `cueOut`. Storing it relative to `cueIn` is the obvious-looking
|
|
122
|
+
* choice and it is wrong: everything downstream seeks in file time, so a relative
|
|
123
|
+
* figure has to be re-based at every read, and eventually one read does not.
|
|
124
|
+
*
|
|
125
|
+
* Two lengths fall out — `intro = introEnd - cueIn` and
|
|
126
|
+
* `outro = cueOut - outroStart` — and they are what a transition is actually
|
|
127
|
+
* sized from. Neither is stored, because a stored derivation is a second thing
|
|
128
|
+
* that can disagree with the first.
|
|
129
|
+
*/
|
|
130
|
+
export interface TrackCuePoints {
|
|
131
|
+
/** Where audio actually starts, past the leading silence. */
|
|
132
|
+
cueIn: number;
|
|
133
|
+
/**
|
|
134
|
+
* Where the record is fully underway: the beat established, or the vocal in.
|
|
135
|
+
*
|
|
136
|
+
* The talk-up limit, and one of the two points that is real work. A detector
|
|
137
|
+
* weighted to low frequencies places this early on a record that opens with
|
|
138
|
+
* a pad, which reads as "the intro is over" while it plainly is not.
|
|
139
|
+
*/
|
|
140
|
+
introEnd: number;
|
|
141
|
+
/**
|
|
142
|
+
* Where the ending begins, so the earliest a blend may start.
|
|
143
|
+
*
|
|
144
|
+
* The other real one, and the one with a specific failure to design against:
|
|
145
|
+
* a low-frequency-weighted detector places it too early on a quiet ending,
|
|
146
|
+
* which is exactly the case an ending-aware transition exists to serve.
|
|
147
|
+
*/
|
|
148
|
+
outroStart: number;
|
|
149
|
+
/** Where audio actually stops, before the trailing silence. */
|
|
150
|
+
cueOut: number;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* How loud the record is, and how close it already runs to its ceiling.
|
|
154
|
+
*
|
|
155
|
+
* Every field is OPTIONAL, and absent is a real answer rather than a gap: a
|
|
156
|
+
* silent or near-silent track has no loudness, and the alternative to omitting
|
|
157
|
+
* it is a floor value like -80 that a caller would then "correct" by fifty
|
|
158
|
+
* decibels. Absent means no opinion, which is what every consumer of these
|
|
159
|
+
* measurements already has to handle.
|
|
160
|
+
*
|
|
161
|
+
* They are also optional in the weaker sense that an analyzer may not compute
|
|
162
|
+
* them at all. A plugin that only finds cue points is a valid analyzer; a
|
|
163
|
+
* station reading these has to degrade to its live normalizer, which is what it
|
|
164
|
+
* does today anyway.
|
|
165
|
+
*/
|
|
166
|
+
export interface TrackLoudness {
|
|
167
|
+
/**
|
|
168
|
+
* Gated programme loudness in LUFS, to ITU-R BS.1770.
|
|
169
|
+
*
|
|
170
|
+
* Gated, which is the whole difference between this and an average level: a
|
|
171
|
+
* record with a long quiet outro is as loud as its body, not as loud as its
|
|
172
|
+
* mean. The station's per-track gain is the distance from this to whatever
|
|
173
|
+
* target it holds.
|
|
174
|
+
*/
|
|
175
|
+
integratedLufs?: number;
|
|
176
|
+
/**
|
|
177
|
+
* The highest inter-sample peak in dBTP, which is what caps a boost.
|
|
178
|
+
*
|
|
179
|
+
* Distinct from {@link samplePeakDb} and the distinction is the point: the
|
|
180
|
+
* reconstructed waveform between two samples can exceed both of them,
|
|
181
|
+
* routinely by around a decibel. A gain computed against sample peak alone
|
|
182
|
+
* is how a quiet master gets lifted into clipping, so this is the number a
|
|
183
|
+
* boost has to respect.
|
|
184
|
+
*
|
|
185
|
+
* Legitimately positive. A value above 0 dBTP means the master already
|
|
186
|
+
* overshoots on playback, which is worth knowing before adding anything.
|
|
187
|
+
*/
|
|
188
|
+
truePeakDb?: number;
|
|
189
|
+
/**
|
|
190
|
+
* The highest actual sample, in dBFS.
|
|
191
|
+
*
|
|
192
|
+
* Carried alongside the true peak rather than instead of it, because the gap
|
|
193
|
+
* between them is diagnostic: a wide one means the master is already fighting
|
|
194
|
+
* its own ceiling.
|
|
195
|
+
*/
|
|
196
|
+
samplePeakDb?: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* What the FILE says about its own loudness, as opposed to what was measured.
|
|
200
|
+
*
|
|
201
|
+
* A different kind of claim from {@link TrackLoudness}, which is why it is a
|
|
202
|
+
* different interface: those fields are this analyzer's opinion, and these are
|
|
203
|
+
* whoever mastered or scanned the record telling the station what they decided.
|
|
204
|
+
* A consumer that prefers one over the other has to be able to tell them apart,
|
|
205
|
+
* which it cannot do if they arrive in the same field.
|
|
206
|
+
*
|
|
207
|
+
* All optional, and most files carry none of them.
|
|
208
|
+
*/
|
|
209
|
+
export interface TrackTaggedLoudness {
|
|
210
|
+
/**
|
|
211
|
+
* The gain the file's own tags ask for, in dB.
|
|
212
|
+
*
|
|
213
|
+
* **Meaningless without {@link tagReferenceLufs}**, and that is the whole
|
|
214
|
+
* reason both are reported. A gain is a correction relative to some level,
|
|
215
|
+
* and the two conventions in the wild are five decibels apart, so a station
|
|
216
|
+
* that stored only this would be storing the answer to a question it can no
|
|
217
|
+
* longer ask.
|
|
218
|
+
*/
|
|
219
|
+
tagGainDb?: number;
|
|
220
|
+
/**
|
|
221
|
+
* The loudness {@link tagGainDb} is relative to, in LUFS.
|
|
222
|
+
*
|
|
223
|
+
* -23 for an R128 tag, where the specification fixes it. -18 for a
|
|
224
|
+
* ReplayGain tag, where it is an ASSUMPTION: ReplayGain 2.0 targets -18 and
|
|
225
|
+
* every current scanner writes it, but the older convention used the same
|
|
226
|
+
* tag names with no version field, so a file carrying it is
|
|
227
|
+
* indistinguishable from the outside.
|
|
228
|
+
*
|
|
229
|
+
* Subtracting this pair gives the loudness the tagger believed the record
|
|
230
|
+
* has, which is the figure a station's own target applies to.
|
|
231
|
+
*/
|
|
232
|
+
tagReferenceLufs?: number;
|
|
233
|
+
/**
|
|
234
|
+
* The peak the file's tags declare, in dBFS.
|
|
235
|
+
*
|
|
236
|
+
* A SAMPLE peak, always, because that is what ReplayGain defines. It is not
|
|
237
|
+
* a substitute for {@link TrackLoudness.truePeakDb} and nothing should cap a
|
|
238
|
+
* boost with it; it is stored because the gap between the two says how hard
|
|
239
|
+
* the master is already running.
|
|
240
|
+
*/
|
|
241
|
+
tagPeakDb?: number;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What one analysis produced.
|
|
245
|
+
*
|
|
246
|
+
* `data` is deliberately the only place measurements live, and the host stores
|
|
247
|
+
* it whole without reading the individual fields. That is what lets a later
|
|
248
|
+
* schema version add a beat grid or a vocal curve without touching the plugin
|
|
249
|
+
* contract, the host, or the database.
|
|
250
|
+
*/
|
|
251
|
+
export interface TrackAnalysis {
|
|
252
|
+
/**
|
|
253
|
+
* The shape of {@link data}, as this plugin actually produced it.
|
|
254
|
+
*
|
|
255
|
+
* Report what was measured rather than {@link ANALYSIS_SCHEMA_VERSION}: an
|
|
256
|
+
* adapter over a separate analyzer is reporting that analyzer's version, and
|
|
257
|
+
* the two drift the moment one of them is upgraded and the other is not. A
|
|
258
|
+
* version the host does not know is a configuration problem it can name,
|
|
259
|
+
* where a wrong one is a row nothing can read and nothing can explain.
|
|
260
|
+
*/
|
|
261
|
+
schemaVersion: number;
|
|
262
|
+
/**
|
|
263
|
+
* Whether the whole file was measured.
|
|
264
|
+
*
|
|
265
|
+
* **Load-bearing, and the host cannot check it.** A byte-capped, idle-timed
|
|
266
|
+
* out or otherwise truncated download produces perfectly confident
|
|
267
|
+
* measurements of a file that was never the track, and the specific lie it
|
|
268
|
+
* tells is that a record which fades ended cold. Since the host never sees
|
|
269
|
+
* the bytes, this is the only signal that separates a measurement worth
|
|
270
|
+
* keeping from one worth discarding, and a plugin that always answers `true`
|
|
271
|
+
* has quietly disabled the check.
|
|
272
|
+
*
|
|
273
|
+
* A genuinely short track is `true`. A download that stopped early is
|
|
274
|
+
* `false`, and the two are told apart with {@link AnalysisRef.durationMs}
|
|
275
|
+
* where there is one.
|
|
276
|
+
*/
|
|
277
|
+
complete: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* The measurements, in the shape {@link schemaVersion} names.
|
|
280
|
+
*
|
|
281
|
+
* Typed as the v1 fields plus room to grow rather than as a closed
|
|
282
|
+
* interface, because the host passes it through unread. A v2 payload with a
|
|
283
|
+
* tempo and a downbeat grid is the same call, the same plugin method, and a
|
|
284
|
+
* different number above.
|
|
285
|
+
*
|
|
286
|
+
* The cue points are required and the loudness is not, which reflects what
|
|
287
|
+
* each costs to produce: the points come from the decode that has already
|
|
288
|
+
* happened, where loudness needs a filter chain an analyzer may reasonably
|
|
289
|
+
* not implement.
|
|
290
|
+
*/
|
|
291
|
+
data: TrackCuePoints & TrackLoudness & TrackTaggedLoudness & Record<string, unknown>;
|
|
292
|
+
/**
|
|
293
|
+
* How long the audio turned out to be once decoded.
|
|
294
|
+
*
|
|
295
|
+
* The honest figure, as opposed to the catalog's claim in
|
|
296
|
+
* {@link AnalysisRef.durationMs}. Worth reporting even when the two agree:
|
|
297
|
+
* where they do not, this is the one that the offsets above are on the same
|
|
298
|
+
* timeline as.
|
|
299
|
+
*/
|
|
300
|
+
durationMs?: number;
|
|
301
|
+
/**
|
|
302
|
+
* What did the measuring, as a name and version.
|
|
303
|
+
*
|
|
304
|
+
* Stored as provenance, so a row can be attributed after the fact when a
|
|
305
|
+
* detector turns out to have been wrong about a class of records. Free-text
|
|
306
|
+
* and never parsed.
|
|
307
|
+
*/
|
|
308
|
+
analyzer?: string;
|
|
309
|
+
}
|
|
310
|
+
/** A plugin that can measure a track's audio. */
|
|
311
|
+
export interface AnalysisProvider extends PluginLifecycle {
|
|
312
|
+
/**
|
|
313
|
+
* Measure one track.
|
|
314
|
+
*
|
|
315
|
+
* One track per call, with no batch sibling: the unit of work is one file's
|
|
316
|
+
* bytes, and there is no upstream round trip to amortise across several. How
|
|
317
|
+
* many run at once is the host's decision, taken against hardware it can see
|
|
318
|
+
* and this plugin cannot.
|
|
319
|
+
*
|
|
320
|
+
* Expect to be given minutes rather than seconds — decoding a full record is
|
|
321
|
+
* not a request, it is a job — but honour `host.signal` all the same, because
|
|
322
|
+
* a station shutting down should not wait on a measurement nobody will read.
|
|
323
|
+
*
|
|
324
|
+
* @throws {PluginError} `config` when the plugin is not set up enough to try
|
|
325
|
+
* (no analyzer address), `upstream` when the audio could not be fetched or
|
|
326
|
+
* could not be decoded, `timeout` when the analyzer did not answer.
|
|
327
|
+
*/
|
|
328
|
+
analyzeTrack(ref: AnalysisRef): Promise<TrackAnalysis>;
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/capabilities/analysis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,cAAc;IAC3B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC1B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,OAAO,CAAC;IAElB;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,cAAc,GAAG,aAAa,GAAG,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAErF;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,iDAAiD;AACjD,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACrD;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC1D"}
|