@hraness/dawg 0.6.1 → 0.8.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/CHANGELOG.md +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +82 -32
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/agent/gateway.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isProviderModelId } from "../auth/credentials.ts";
|
|
2
|
+
import { readSseData, SseBudgetError } from "./sse.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The two original model aliases. A model is either one of these or an exact
|
|
@@ -18,8 +19,6 @@ export const DEFAULT_MODEL_IDS: Readonly<Record<GatewayModel, string>> =
|
|
|
18
19
|
"sol-6.1": "openai/gpt-6.1-sol",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
|
-
const MODEL_ID_PATTERN =
|
|
22
|
-
/^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
23
22
|
const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
24
23
|
|
|
25
24
|
export function isGatewayModel(value: unknown): value is GatewayModel {
|
|
@@ -36,21 +35,22 @@ export function isGatewayModel(value: unknown): value is GatewayModel {
|
|
|
36
35
|
export function resolveModelId(
|
|
37
36
|
alias: string,
|
|
38
37
|
overrides: Partial<Record<GatewayModel, string | undefined>> = {},
|
|
38
|
+
provider: ApiProvider = "gateway",
|
|
39
39
|
): string {
|
|
40
40
|
if (!isGatewayModel(alias)) {
|
|
41
|
-
if (
|
|
41
|
+
if (isProviderModelId(alias, provider)) return alias;
|
|
42
42
|
throw new Error(
|
|
43
43
|
`unknown model "${alias.slice(0, 32)}"; use ${GATEWAY_MODELS.join(" or ")} or a vendor/model ID`,
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
const id = overrides[alias] ?? DEFAULT_MODEL_IDS[alias];
|
|
47
|
-
if (!
|
|
47
|
+
if (!isProviderModelId(id, provider))
|
|
48
48
|
throw new Error(`model ID for ${alias} must look like provider/model`);
|
|
49
49
|
return id;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function checkedModelId(id: string): string {
|
|
53
|
-
if (!
|
|
52
|
+
function checkedModelId(id: string, provider: ApiProvider): string {
|
|
53
|
+
if (!isProviderModelId(id, provider))
|
|
54
54
|
throw new Error("model ID must look like provider/model");
|
|
55
55
|
return id;
|
|
56
56
|
}
|
|
@@ -85,6 +85,11 @@ export type ChatStreamRequest = {
|
|
|
85
85
|
modelId?: string;
|
|
86
86
|
maxTokens?: number;
|
|
87
87
|
maxResponseBytes: number;
|
|
88
|
+
/**
|
|
89
|
+
* A model ID to try once when every attempt on the main model failed
|
|
90
|
+
* before producing a byte (a 5xx, a header timeout, a stalled stream).
|
|
91
|
+
*/
|
|
92
|
+
fallbackModelId?: string;
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
/** Normalized stream events; provider chunk shapes never leave this module. */
|
|
@@ -145,6 +150,12 @@ export const GATEWAY_RETRIES = 2;
|
|
|
145
150
|
const RETRY_BASE_MS = 500;
|
|
146
151
|
/** Response headers must arrive within this long, per attempt. */
|
|
147
152
|
export const GATEWAY_HEADER_TIMEOUT_MS = 15_000;
|
|
153
|
+
/**
|
|
154
|
+
* After headers, the first stream event must arrive within this long, per
|
|
155
|
+
* attempt. A provider that accepts the request and then sends nothing is
|
|
156
|
+
* retried like a header timeout: no byte has reached the caller yet.
|
|
157
|
+
*/
|
|
158
|
+
export const GATEWAY_FIRST_BYTE_TIMEOUT_MS = 20_000;
|
|
148
159
|
|
|
149
160
|
export type GatewayRetryOptions = Readonly<{
|
|
150
161
|
retries?: number;
|
|
@@ -174,6 +185,7 @@ export type ApiClientOptions = {
|
|
|
174
185
|
fetcher?: GatewayFetcher;
|
|
175
186
|
retry?: GatewayRetryOptions;
|
|
176
187
|
headerTimeoutMs?: number;
|
|
188
|
+
firstByteTimeoutMs?: number;
|
|
177
189
|
};
|
|
178
190
|
|
|
179
191
|
/** The streaming tool-calling client for OpenRouter's OpenAI-compatible API. */
|
|
@@ -212,6 +224,8 @@ function createApiClient(
|
|
|
212
224
|
const sleep = options.retry?.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
213
225
|
const random = options.retry?.random ?? Math.random;
|
|
214
226
|
const headerTimeoutMs = options.headerTimeoutMs ?? GATEWAY_HEADER_TIMEOUT_MS;
|
|
227
|
+
const firstByteTimeoutMs =
|
|
228
|
+
options.firstByteTimeoutMs ?? GATEWAY_FIRST_BYTE_TIMEOUT_MS;
|
|
215
229
|
const overrides: Partial<Record<GatewayModel, string | undefined>> = {
|
|
216
230
|
"opus-5.5": process.env.DAWG_OPUS_MODEL || undefined,
|
|
217
231
|
"sol-6.1": process.env.DAWG_SOL_MODEL || undefined,
|
|
@@ -222,50 +236,82 @@ function createApiClient(
|
|
|
222
236
|
|
|
223
237
|
return {
|
|
224
238
|
provider,
|
|
225
|
-
modelId: (model) => resolveModelId(model, overrides),
|
|
239
|
+
modelId: (model) => resolveModelId(model, overrides, provider),
|
|
226
240
|
async *stream(request, signal) {
|
|
227
|
-
const
|
|
241
|
+
const primary =
|
|
228
242
|
request.modelId !== undefined
|
|
229
|
-
? checkedModelId(request.modelId)
|
|
230
|
-
: resolveModelId(request.model, overrides);
|
|
243
|
+
? checkedModelId(request.modelId, provider)
|
|
244
|
+
: resolveModelId(request.model, overrides, provider);
|
|
245
|
+
const fallback =
|
|
246
|
+
request.fallbackModelId !== undefined &&
|
|
247
|
+
request.fallbackModelId !== primary
|
|
248
|
+
? checkedModelId(request.fallbackModelId, provider)
|
|
249
|
+
: undefined;
|
|
231
250
|
if (!apiKey)
|
|
232
251
|
throw new GatewayError(
|
|
233
252
|
provider === "openrouter"
|
|
234
|
-
? "no OpenRouter key; run `dawg
|
|
235
|
-
: "no AI Gateway key; run `dawg
|
|
253
|
+
? "no OpenRouter key; run `dawg model key openrouter` or set OPENROUTER_API_KEY"
|
|
254
|
+
: "no AI Gateway key; run `dawg model key` or set AI_GATEWAY_API_KEY",
|
|
236
255
|
);
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
const encodeFor = (model: string): string => {
|
|
257
|
+
const body: Record<string, unknown> = {
|
|
258
|
+
model,
|
|
259
|
+
messages: withPromptCache(provider, model, request.messages),
|
|
260
|
+
stream: true,
|
|
261
|
+
// The final chunk then carries token usage (and OpenRouter's cost).
|
|
262
|
+
stream_options: { include_usage: true },
|
|
263
|
+
};
|
|
264
|
+
if (provider === "openrouter") body.usage = { include: true };
|
|
265
|
+
// The Gateway adds Anthropic cache breakpoints itself when asked; it
|
|
266
|
+
// documents the option as a no-op for implicitly caching providers.
|
|
267
|
+
else body.providerOptions = { gateway: { caching: "auto" } };
|
|
268
|
+
if (request.tools && request.tools.length > 0) {
|
|
269
|
+
body.tools = request.tools;
|
|
270
|
+
body.tool_choice = "auto";
|
|
271
|
+
}
|
|
272
|
+
if (request.temperature !== undefined)
|
|
273
|
+
body.temperature = request.temperature;
|
|
274
|
+
if (
|
|
275
|
+
request.maxTokens !== undefined &&
|
|
276
|
+
Number.isInteger(request.maxTokens) &&
|
|
277
|
+
request.maxTokens > 0
|
|
278
|
+
)
|
|
279
|
+
body.max_tokens = Math.min(request.maxTokens, 4096);
|
|
280
|
+
return JSON.stringify(body);
|
|
243
281
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
body.tools = request.tools;
|
|
247
|
-
body.tool_choice = "auto";
|
|
248
|
-
}
|
|
249
|
-
if (request.temperature !== undefined)
|
|
250
|
-
body.temperature = request.temperature;
|
|
251
|
-
if (
|
|
252
|
-
request.maxTokens !== undefined &&
|
|
253
|
-
Number.isInteger(request.maxTokens) &&
|
|
254
|
-
request.maxTokens > 0
|
|
255
|
-
)
|
|
256
|
-
body.max_tokens = Math.min(request.maxTokens, 4096);
|
|
257
|
-
const encoded = JSON.stringify(body);
|
|
282
|
+
let model = primary;
|
|
283
|
+
let encoded = encodeFor(model);
|
|
258
284
|
// Retry only before any byte of a response has been consumed: a
|
|
259
285
|
// network failure, a header-phase timeout, or 408/429/5xx. Once the
|
|
260
286
|
// stream is flowing, a failure surfaces as-is (replaying could repeat
|
|
261
287
|
// tool calls the model already made).
|
|
262
|
-
let
|
|
288
|
+
let events: AsyncIterator<string> | undefined;
|
|
289
|
+
let first: IteratorResult<string> | undefined;
|
|
290
|
+
const streamOptions: { maxBytes: number; signal?: AbortSignal } = {
|
|
291
|
+
maxBytes: request.maxResponseBytes,
|
|
292
|
+
};
|
|
263
293
|
for (let attempt = 0; ; attempt += 1) {
|
|
264
294
|
if (signal?.aborted) throw abortError(signal);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
295
|
+
// The header budget bounds only the wait for response headers. A
|
|
296
|
+
// fetch signal also governs the body, so the timer is cleared as soon
|
|
297
|
+
// as headers arrive; after that only the caller's signal (the user or
|
|
298
|
+
// the turn budget) can cut the stream short.
|
|
299
|
+
const headerController = new AbortController();
|
|
300
|
+
const headerTimer = setTimeout(
|
|
301
|
+
() =>
|
|
302
|
+
headerController.abort(
|
|
303
|
+
new DOMException(
|
|
304
|
+
`${name} did not respond within ${headerTimeoutMs} ms`,
|
|
305
|
+
"TimeoutError",
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
headerTimeoutMs,
|
|
309
|
+
);
|
|
310
|
+
const headerSignal = headerController.signal;
|
|
311
|
+
const requestSignal =
|
|
312
|
+
signal === undefined
|
|
313
|
+
? headerSignal
|
|
314
|
+
: AbortSignal.any([signal, headerSignal]);
|
|
269
315
|
const init: RequestInit = {
|
|
270
316
|
method: "POST",
|
|
271
317
|
headers: {
|
|
@@ -275,57 +321,197 @@ function createApiClient(
|
|
|
275
321
|
...(provider === "openrouter" ? OPENROUTER_HEADERS : {}),
|
|
276
322
|
},
|
|
277
323
|
body: encoded,
|
|
278
|
-
signal:
|
|
324
|
+
signal: requestSignal,
|
|
279
325
|
};
|
|
280
326
|
let reason: string;
|
|
281
327
|
try {
|
|
282
|
-
|
|
328
|
+
let candidate: Response;
|
|
329
|
+
try {
|
|
330
|
+
candidate = await fetcher(`${baseUrl}/chat/completions`, init);
|
|
331
|
+
} finally {
|
|
332
|
+
clearTimeout(headerTimer);
|
|
333
|
+
}
|
|
283
334
|
if (candidate.ok) {
|
|
284
|
-
|
|
285
|
-
|
|
335
|
+
if (!candidate.body)
|
|
336
|
+
throw new GatewayError(`${name} returned an empty stream`);
|
|
337
|
+
// Wait for the first event under its own budget; the stall
|
|
338
|
+
// signal is never fired once it has arrived.
|
|
339
|
+
const stall = new AbortController();
|
|
340
|
+
const stallTimer = setTimeout(
|
|
341
|
+
() =>
|
|
342
|
+
stall.abort(
|
|
343
|
+
new DOMException(
|
|
344
|
+
`${name} sent nothing within ${firstByteTimeoutMs} ms`,
|
|
345
|
+
"TimeoutError",
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
firstByteTimeoutMs,
|
|
349
|
+
);
|
|
350
|
+
streamOptions.signal =
|
|
351
|
+
signal === undefined
|
|
352
|
+
? stall.signal
|
|
353
|
+
: AbortSignal.any([signal, stall.signal]);
|
|
354
|
+
// Any byte, a keep-alive comment included, proves the provider
|
|
355
|
+
// is alive and disarms the stall timer.
|
|
356
|
+
const watched = candidate.body.pipeThrough(
|
|
357
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
358
|
+
transform(chunk, controller) {
|
|
359
|
+
clearTimeout(stallTimer);
|
|
360
|
+
controller.enqueue(chunk);
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
const iterator = readSseData(watched, streamOptions)[
|
|
365
|
+
Symbol.asyncIterator
|
|
366
|
+
]();
|
|
367
|
+
try {
|
|
368
|
+
first = await iterator.next();
|
|
369
|
+
events = iterator;
|
|
370
|
+
break;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (!stall.signal.aborted || signal?.aborted) throw error;
|
|
373
|
+
await iterator.return?.(undefined).catch(() => undefined);
|
|
374
|
+
if (attempt >= retries) {
|
|
375
|
+
if (fallback !== undefined && model !== fallback) {
|
|
376
|
+
model = fallback;
|
|
377
|
+
encoded = encodeFor(model);
|
|
378
|
+
attempt = -1;
|
|
379
|
+
yield {
|
|
380
|
+
type: "activity",
|
|
381
|
+
message: `falling back to ${model} (stalled)…`,
|
|
382
|
+
};
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
throw new GatewayError(
|
|
386
|
+
`${name} sent nothing within ${firstByteTimeoutMs} ms`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
reason = "stalled";
|
|
390
|
+
} finally {
|
|
391
|
+
clearTimeout(stallTimer);
|
|
392
|
+
}
|
|
393
|
+
yield { type: "activity", message: `retrying (${reason})…` };
|
|
394
|
+
await sleep(
|
|
395
|
+
Math.round(retryBaseMs * 2 ** attempt * (0.5 + random())),
|
|
396
|
+
);
|
|
397
|
+
continue;
|
|
286
398
|
}
|
|
287
399
|
const detail = redact(await boundedErrorDetail(candidate));
|
|
288
400
|
const error = new GatewayError(
|
|
289
401
|
`${name} request failed (${candidate.status})${detail ? `: ${detail}` : ""}`,
|
|
290
402
|
candidate.status,
|
|
291
403
|
);
|
|
292
|
-
if (!retryableStatus(candidate.status)
|
|
293
|
-
|
|
404
|
+
if (!retryableStatus(candidate.status)) throw error;
|
|
405
|
+
if (attempt >= retries) {
|
|
406
|
+
if (fallback === undefined || model === fallback) throw error;
|
|
407
|
+
reason = String(candidate.status);
|
|
408
|
+
model = fallback;
|
|
409
|
+
encoded = encodeFor(model);
|
|
410
|
+
attempt = -1;
|
|
411
|
+
yield {
|
|
412
|
+
type: "activity",
|
|
413
|
+
message: `falling back to ${model} (${reason})…`,
|
|
414
|
+
};
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
294
417
|
reason = String(candidate.status);
|
|
295
418
|
} catch (error) {
|
|
296
|
-
if (error instanceof GatewayError
|
|
419
|
+
if (error instanceof GatewayError || error instanceof SseBudgetError)
|
|
420
|
+
throw error;
|
|
297
421
|
if (signal?.aborted) throw abortError(signal);
|
|
298
|
-
if (attempt >= retries)
|
|
299
|
-
|
|
422
|
+
if (attempt >= retries) {
|
|
423
|
+
const failure = new GatewayError(
|
|
300
424
|
headerSignal.aborted
|
|
301
425
|
? `${name} did not respond within ${headerTimeoutMs} ms`
|
|
302
426
|
: `${name} request failed: ${redact(error instanceof Error ? error.message : String(error))}`,
|
|
303
427
|
);
|
|
428
|
+
if (fallback === undefined || model === fallback) throw failure;
|
|
429
|
+
model = fallback;
|
|
430
|
+
encoded = encodeFor(model);
|
|
431
|
+
attempt = -1;
|
|
432
|
+
yield {
|
|
433
|
+
type: "activity",
|
|
434
|
+
message: `falling back to ${model} (${headerSignal.aborted ? "timeout" : "network"})…`,
|
|
435
|
+
};
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
304
438
|
reason = headerSignal.aborted ? "timeout" : "network";
|
|
305
439
|
}
|
|
306
440
|
yield { type: "activity", message: `retrying (${reason})…` };
|
|
307
441
|
const backoff = retryBaseMs * 2 ** attempt;
|
|
308
442
|
await sleep(Math.round(backoff * (0.5 + random())));
|
|
309
443
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
chunk
|
|
320
|
-
} catch {
|
|
321
|
-
throw new GatewayError(`${name} sent a malformed stream chunk`);
|
|
444
|
+
try {
|
|
445
|
+
for (let next = first; !next.done; next = await events.next()) {
|
|
446
|
+
const data = next.value;
|
|
447
|
+
let chunk: unknown;
|
|
448
|
+
try {
|
|
449
|
+
chunk = JSON.parse(data);
|
|
450
|
+
} catch {
|
|
451
|
+
throw new GatewayError(`${name} sent a malformed stream chunk`);
|
|
452
|
+
}
|
|
453
|
+
yield* normalizeChunk(chunk, redact, name);
|
|
322
454
|
}
|
|
323
|
-
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// A failure while the body streams (a reset connection, a transport
|
|
457
|
+
// timeout) is a provider failure, not an internal one. Budget errors
|
|
458
|
+
// and the caller's own abort keep their identity.
|
|
459
|
+
if (
|
|
460
|
+
error instanceof GatewayError ||
|
|
461
|
+
error instanceof SseBudgetError ||
|
|
462
|
+
signal?.aborted
|
|
463
|
+
)
|
|
464
|
+
throw error;
|
|
465
|
+
const timedOut =
|
|
466
|
+
error instanceof Error && error.name === "TimeoutError";
|
|
467
|
+
throw new GatewayError(
|
|
468
|
+
timedOut
|
|
469
|
+
? `${name} stream timed out`
|
|
470
|
+
: `${name} stream failed: ${redact(error instanceof Error ? error.message : String(error)).slice(0, 300)}`,
|
|
471
|
+
);
|
|
324
472
|
}
|
|
325
473
|
},
|
|
326
474
|
};
|
|
327
475
|
}
|
|
328
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Mark the leading system message as a prompt-cache breakpoint for Anthropic
|
|
479
|
+
* models on OpenRouter and the AI Gateway. Anthropic caches tools, then
|
|
480
|
+
* system, then messages, so a breakpoint on the static system prompt caches
|
|
481
|
+
* the tool schemas and the prompt (about 27K tokens) across every step of a
|
|
482
|
+
* turn; the per-step brief follows it and stays uncached. The Gateway's
|
|
483
|
+
* `caching: "auto"` option alone cached nothing on its chat completions
|
|
484
|
+
* endpoint (bench/agent-eval measured 0 cached tokens over 4M), so the
|
|
485
|
+
* breakpoint is explicit there too. Other providers cache prefixes
|
|
486
|
+
* implicitly, so their messages go out unchanged.
|
|
487
|
+
*/
|
|
488
|
+
export function withPromptCache(
|
|
489
|
+
provider: ApiProvider,
|
|
490
|
+
model: string,
|
|
491
|
+
messages: readonly ChatMessage[],
|
|
492
|
+
): readonly unknown[] {
|
|
493
|
+
const first = messages[0];
|
|
494
|
+
if (
|
|
495
|
+
!model.toLowerCase().startsWith("anthropic/") ||
|
|
496
|
+
first?.role !== "system" ||
|
|
497
|
+
first.content.length === 0
|
|
498
|
+
)
|
|
499
|
+
return messages;
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
role: "system",
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
type: "text",
|
|
506
|
+
text: first.content,
|
|
507
|
+
cache_control: { type: "ephemeral" },
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
},
|
|
511
|
+
...messages.slice(1),
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
|
|
329
515
|
function* normalizeChunk(
|
|
330
516
|
chunk: unknown,
|
|
331
517
|
redact: (text: string) => string,
|
package/src/agent/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { writePrivateJson } from "../auth/credentials.ts";
|
|
3
|
+
import { isProviderModelId, writePrivateJson } from "../auth/credentials.ts";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_MODEL_IDS,
|
|
6
6
|
GATEWAY_BASE_URL,
|
|
@@ -27,6 +27,11 @@ export type CatalogEntry = Readonly<{
|
|
|
27
27
|
class: ModelClass;
|
|
28
28
|
gateway?: string;
|
|
29
29
|
openrouter?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Alias of a model on another vendor to try once when this one fails
|
|
32
|
+
* before its first byte (repeated 5xx, header timeout, stalled stream).
|
|
33
|
+
*/
|
|
34
|
+
fallback?: string;
|
|
30
35
|
}>;
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -71,6 +76,14 @@ export const MODEL_CATALOG: readonly CatalogEntry[] = Object.freeze([
|
|
|
71
76
|
gateway: "anthropic/claude-sonnet-5.5",
|
|
72
77
|
openrouter: "anthropic/claude-sonnet-5.5",
|
|
73
78
|
},
|
|
79
|
+
{
|
|
80
|
+
alias: "haiku-5.5",
|
|
81
|
+
label: "Claude Haiku 5.5",
|
|
82
|
+
class: "fast",
|
|
83
|
+
gateway: "anthropic/claude-haiku-5.5",
|
|
84
|
+
openrouter: "anthropic/claude-haiku-5.5",
|
|
85
|
+
fallback: "glm-5.3-flash",
|
|
86
|
+
},
|
|
74
87
|
{
|
|
75
88
|
alias: "haiku-4.5",
|
|
76
89
|
label: "Claude Haiku 4.5",
|
|
@@ -120,6 +133,13 @@ export const MODEL_CATALOG: readonly CatalogEntry[] = Object.freeze([
|
|
|
120
133
|
gateway: "zai/glm-5.3",
|
|
121
134
|
openrouter: "z-ai/glm-5.3",
|
|
122
135
|
},
|
|
136
|
+
{
|
|
137
|
+
alias: "glm-5.3-flash",
|
|
138
|
+
label: "GLM-5.3 Flash",
|
|
139
|
+
class: "fast",
|
|
140
|
+
gateway: "zai/glm-5.3-flash",
|
|
141
|
+
openrouter: "z-ai/glm-5.3-flash",
|
|
142
|
+
},
|
|
123
143
|
{
|
|
124
144
|
alias: "llama-4-maverick",
|
|
125
145
|
label: "Llama 4 Maverick",
|
|
@@ -129,28 +149,48 @@ export const MODEL_CATALOG: readonly CatalogEntry[] = Object.freeze([
|
|
|
129
149
|
},
|
|
130
150
|
]);
|
|
131
151
|
|
|
132
|
-
const MODEL_ID_PATTERN =
|
|
133
|
-
/^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
134
|
-
|
|
135
152
|
export function defaultModelId(provider: ApiProvider): string {
|
|
136
153
|
return provider === "openrouter"
|
|
137
154
|
? MODEL_CATALOG[0]!.openrouter!
|
|
138
155
|
: DEFAULT_MODEL_IDS["opus-5.5"];
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* `/model fast` picks this alias: the fastest model measured by
|
|
160
|
+
* bench/agent-eval that passes the non-style tasks about as well as the
|
|
161
|
+
* default (see docs/model-eval.md).
|
|
162
|
+
*/
|
|
163
|
+
export const FAST_MODEL_ALIAS = "haiku-5.5";
|
|
164
|
+
|
|
165
|
+
/** The provider ID to fall back to for `id`, if its catalog row names one. */
|
|
166
|
+
export function fallbackModelId(
|
|
167
|
+
provider: ApiProvider,
|
|
168
|
+
id: string,
|
|
169
|
+
): string | undefined {
|
|
170
|
+
const row = MODEL_CATALOG.find((entry) => entry[provider] === id);
|
|
171
|
+
if (!row?.fallback) return undefined;
|
|
172
|
+
return MODEL_CATALOG.find((entry) => entry.alias === row.fallback)?.[
|
|
173
|
+
provider
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* An alias (`fast` included), a catalog label, or an exact `vendor/model` ID
|
|
179
|
+
* → the provider's ID.
|
|
180
|
+
*/
|
|
142
181
|
export function resolveModelChoice(
|
|
143
182
|
provider: ApiProvider,
|
|
144
183
|
input: string,
|
|
145
184
|
): string | undefined {
|
|
146
185
|
const value = input.trim();
|
|
147
186
|
const lower = value.toLowerCase();
|
|
187
|
+
if (lower === "fast") return resolveModelChoice(provider, FAST_MODEL_ALIAS);
|
|
148
188
|
const entry = MODEL_CATALOG.find(
|
|
149
189
|
(row) => row.alias === lower || row.label.toLowerCase() === lower,
|
|
150
190
|
);
|
|
151
191
|
if (entry) return entry[provider];
|
|
152
192
|
if (isGatewayModel(lower)) return DEFAULT_MODEL_IDS[lower];
|
|
153
|
-
return
|
|
193
|
+
return isProviderModelId(value, provider) ? value : undefined;
|
|
154
194
|
}
|
|
155
195
|
|
|
156
196
|
/** `anthropic/claude-opus-5.5` → `opus-5.5`; unknown IDs show as themselves. */
|
|
@@ -397,7 +437,7 @@ export function parseLiveCatalog(
|
|
|
397
437
|
if (
|
|
398
438
|
!isRecord(row) ||
|
|
399
439
|
typeof row.id !== "string" ||
|
|
400
|
-
!
|
|
440
|
+
!isProviderModelId(row.id, provider)
|
|
401
441
|
)
|
|
402
442
|
continue;
|
|
403
443
|
const pricing = isRecord(row.pricing) ? row.pricing : {};
|
|
@@ -486,17 +526,18 @@ export async function loadLiveCatalog(
|
|
|
486
526
|
* and `chatTools()` serialized, plus `compositionBrief` of a 3-track, 48-note
|
|
487
527
|
* loop; see models.test.ts, which keeps these in step with the code):
|
|
488
528
|
*
|
|
489
|
-
* - system prompt ≈
|
|
490
|
-
* (capped at 12 KiB), user request ≈ 0.1 KB → ≈
|
|
491
|
-
*
|
|
529
|
+
* - system prompt ≈ 11 KB, tool schemas ≈ 70 KB, brief ≈ 1.6 KB typical
|
|
530
|
+
* (capped at 12 KiB), user request ≈ 0.1 KB → ≈ 83 KB per request. JSON
|
|
531
|
+
* schemas tokenize at about 3 bytes per token: the agent eval
|
|
532
|
+
* (bench/agent-eval) measured 26,500 input tokens per request on Claude;
|
|
492
533
|
* - about 2 requests per prompt (tool calls, then a short reply), the second
|
|
493
534
|
* also carrying the first step's tool calls and results (≈ 0.6 KB);
|
|
494
535
|
* - output ≈ 450 tokens of tool-call arguments and ≈ 150 of reply.
|
|
495
536
|
*
|
|
496
|
-
* So ≈
|
|
537
|
+
* So ≈ 53,000 input and ≈ 600 output tokens per prompt.
|
|
497
538
|
*/
|
|
498
539
|
export const TYPICAL_PROMPT = Object.freeze({
|
|
499
|
-
inputTokens:
|
|
540
|
+
inputTokens: 53_000,
|
|
500
541
|
outputTokens: 600,
|
|
501
542
|
});
|
|
502
543
|
|
package/src/agent/ops.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveInstrumentWord } from "../../core/instruments.ts";
|
|
2
|
+
import type { TrackVocoder } from "../../core/vocoder.ts";
|
|
2
3
|
import { pitchToMidi } from "../../core/pitch.ts";
|
|
3
4
|
import {
|
|
4
5
|
instrumentPatchForWord,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
type TrackWind,
|
|
7
8
|
} from "../../core/resonators.ts";
|
|
8
9
|
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
10
|
+
import type { TrackSing } from "../../core/sing.ts";
|
|
9
11
|
|
|
10
12
|
export { pitchToMidi };
|
|
11
13
|
|
|
@@ -50,6 +52,8 @@ export type AgentOperation =
|
|
|
50
52
|
};
|
|
51
53
|
/** The instrument word as typed (a guitar alias also loads its rig). */
|
|
52
54
|
word?: string;
|
|
55
|
+
/** `volume drums 0.5`: another track than the focused one. */
|
|
56
|
+
trackId?: string;
|
|
53
57
|
}
|
|
54
58
|
| {
|
|
55
59
|
type: "automation";
|
|
@@ -99,6 +103,19 @@ export function parsePrompt(prompt: string): AgentOperation | undefined {
|
|
|
99
103
|
return { type: "track", patch: { muted: false } };
|
|
100
104
|
const volume = text.match(/^(?:volume|vol)\s+(0(?:\.\d+)?|1(?:\.0+)?)$/);
|
|
101
105
|
if (volume) return { type: "track", patch: { volume: Number(volume[1]) } };
|
|
106
|
+
// `volume drums 0.5` / `pan bass -0.2`: the mixer page's rows name their
|
|
107
|
+
// track, so a level is set without moving the focus.
|
|
108
|
+
const level = text.match(
|
|
109
|
+
/^(volume|vol|pan)\s+([a-z0-9][a-z0-9._-]{0,63})\s+(-?1(?:\.0+)?|-?0(?:\.\d+)?)$/,
|
|
110
|
+
);
|
|
111
|
+
if (level && !(level[1] !== "pan" && level[3]!.startsWith("-"))) {
|
|
112
|
+
const value = Number(level[3]);
|
|
113
|
+
return {
|
|
114
|
+
type: "track",
|
|
115
|
+
patch: level[1] === "pan" ? { pan: value } : { volume: value },
|
|
116
|
+
trackId: level[2]!,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
102
119
|
const clearAutomation = text.match(
|
|
103
120
|
/^(?:clear|reset)\s+(?:(volume|pan)\s+)?automation$/,
|
|
104
121
|
);
|
|
@@ -208,10 +225,19 @@ export function instrumentPatch(word: string): {
|
|
|
208
225
|
string?: { preset: string };
|
|
209
226
|
modal?: TrackModal;
|
|
210
227
|
wind?: TrackWind;
|
|
228
|
+
sing?: TrackSing;
|
|
229
|
+
vocoder?: TrackVocoder;
|
|
211
230
|
} {
|
|
212
231
|
const meaning = resolveInstrumentWord(word);
|
|
213
232
|
if (!meaning) return { instrument: word };
|
|
214
|
-
|
|
233
|
+
// f07-vocoder: the built-in carrier needs its (empty) field.
|
|
234
|
+
if (meaning.field === "vocoder")
|
|
235
|
+
return { instrument: meaning.instrument, vocoder: Object.freeze({}) };
|
|
236
|
+
if (
|
|
237
|
+
meaning.field === "modal" ||
|
|
238
|
+
meaning.field === "wind" ||
|
|
239
|
+
meaning.field === "sing"
|
|
240
|
+
)
|
|
215
241
|
return instrumentPatchForWord(word);
|
|
216
242
|
if (meaning.field === "string" && meaning.preset)
|
|
217
243
|
return {
|
package/src/agent/pack-tools.ts
CHANGED
|
@@ -224,7 +224,7 @@ export const PACK_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
224
224
|
{
|
|
225
225
|
name: "use_sound",
|
|
226
226
|
description:
|
|
227
|
-
'Put a pack sound on a track through the sampler. sound is <pack>/<sound>[:<n>] (n picks a file, like Strudel s("bd:3")), a kit/bank name or Strudel bank nickname (909, 808, linn, TR909, tr808, sp12, dmx, RolandTR909, tidal-drum-machines/RolandTR707) to load a whole
|
|
227
|
+
'Put a pack sound on a track through the sampler. sound is <pack>/<sound>[:<n>] (n picks a file, like Strudel s("bd:3")), a kit/bank name or Strudel bank nickname (909, 808, linn, TR909, tr808, sp12, dmx, RolandTR909, tidal-drum-machines/RolandTR707) to load a whole kit, or a keyed instrument such as gm/gm_acoustic_grand_piano or piano/piano. The file is fetched and pinned by sha256 so renders stay reproducible. Drum hits on the track keep playing the matching kit voice.',
|
|
228
228
|
parameters: {
|
|
229
229
|
type: "object",
|
|
230
230
|
properties: {
|
package/src/agent/planner.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { normalizeSongStyle } from "../../core/style-provenance.ts";
|
|
1
2
|
import { normalizeSynth } from "../../core/synth.ts";
|
|
2
3
|
import { normalizeSongTime, normalizeTrackTime } from "../../core/tempo.ts";
|
|
3
4
|
import { normalizeFx } from "../../core/fx.ts";
|
|
4
5
|
import { normalizeMaster } from "../../core/master.ts";
|
|
5
6
|
import {
|
|
7
|
+
CALIBRATION_LATEST,
|
|
6
8
|
SCORE_LIMITS,
|
|
7
9
|
automationRange,
|
|
8
10
|
isAutomationParameter,
|
|
@@ -142,9 +144,20 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
142
144
|
const time = value.time === null ? null : normalizeSongTime(value.time);
|
|
143
145
|
return { type: "setTime", time: time ?? null };
|
|
144
146
|
}
|
|
147
|
+
if (
|
|
148
|
+
value.type === "setCalibration" &&
|
|
149
|
+
(value.calibration === null ||
|
|
150
|
+
(typeof value.calibration === "number" &&
|
|
151
|
+
Number.isInteger(value.calibration) &&
|
|
152
|
+
value.calibration >= 0 &&
|
|
153
|
+
value.calibration <= CALIBRATION_LATEST))
|
|
154
|
+
)
|
|
155
|
+
return { type: "setCalibration", calibration: value.calibration };
|
|
145
156
|
// The master reuses its bounded validator; null removes it.
|
|
146
157
|
if (value.type === "setMaster" && value.master !== undefined)
|
|
147
158
|
return { type: "setMaster", master: normalizeMaster(value.master) ?? null };
|
|
159
|
+
if (value.type === "setStyle" && value.style !== undefined)
|
|
160
|
+
return { type: "setStyle", style: normalizeSongStyle(value.style) ?? null };
|
|
148
161
|
if (
|
|
149
162
|
value.type === "setSections" &&
|
|
150
163
|
Array.isArray(value.sections) &&
|
|
@@ -165,6 +178,18 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
165
178
|
loopSection: (value.loopSection ?? null) as string | null,
|
|
166
179
|
};
|
|
167
180
|
}
|
|
181
|
+
if (
|
|
182
|
+
value.type === "setLoop" &&
|
|
183
|
+
(value.loop === null ||
|
|
184
|
+
(isRecord(value.loop) &&
|
|
185
|
+
typeof value.loop.startBar === "number" &&
|
|
186
|
+
typeof value.loop.bars === "number"))
|
|
187
|
+
)
|
|
188
|
+
// The reducer checks the range (TrackScore normalizeLoopRange).
|
|
189
|
+
return {
|
|
190
|
+
type: "setLoop",
|
|
191
|
+
loop: value.loop as { startBar: number; bars: number } | null,
|
|
192
|
+
};
|
|
168
193
|
if (
|
|
169
194
|
value.type === "updateTrack" &&
|
|
170
195
|
typeof value.trackId === "string" &&
|