@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool parameter schemas as every provider accepts them.
|
|
3
|
+
*
|
|
4
|
+
* dawg writes unions as `type: ["array", "string"]` with `items` beside it.
|
|
5
|
+
* OpenAI and Anthropic accept that, but Gemini (through the AI Gateway)
|
|
6
|
+
* rewrites the union to `anyOf` and leaves `items` outside, then rejects the
|
|
7
|
+
* whole request, so no Gemini model could run a turn. Here each union
|
|
8
|
+
* becomes an `anyOf` whose branches carry only their own keywords, and an
|
|
9
|
+
* array branch always has `items`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type Schema = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
/** Keywords that belong to one JSON type's branch of a union. */
|
|
15
|
+
const BRANCH_KEYWORDS: Readonly<Record<string, readonly string[]>> = {
|
|
16
|
+
array: ["items", "minItems", "maxItems"],
|
|
17
|
+
object: ["properties", "required", "additionalProperties"],
|
|
18
|
+
string: ["enum", "minLength", "maxLength", "pattern"],
|
|
19
|
+
number: ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"],
|
|
20
|
+
integer: ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"],
|
|
21
|
+
};
|
|
22
|
+
const ALL_BRANCH_KEYWORDS = new Set(Object.values(BRANCH_KEYWORDS).flat());
|
|
23
|
+
|
|
24
|
+
/** Items for an array branch that declared none: any scalar. */
|
|
25
|
+
const ANY_SCALAR_ITEMS: Schema = {
|
|
26
|
+
anyOf: [{ type: "number" }, { type: "string" }, { type: "boolean" }],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function isSchema(value: unknown): value is Schema {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function branch(type: string, schema: Schema): Schema {
|
|
34
|
+
const out: Schema = { type };
|
|
35
|
+
for (const key of BRANCH_KEYWORDS[type] ?? []) {
|
|
36
|
+
if (!(key in schema)) continue;
|
|
37
|
+
const value = schema[key];
|
|
38
|
+
// An enum only constrains the branch whose type its values have.
|
|
39
|
+
if (key === "enum" && Array.isArray(value)) {
|
|
40
|
+
const kept = value.filter((v) => typeof v === "string");
|
|
41
|
+
if (kept.length > 0) out.enum = kept;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
out[key] = value;
|
|
45
|
+
}
|
|
46
|
+
if (type === "array" && out.items === undefined) out.items = ANY_SCALAR_ITEMS;
|
|
47
|
+
return portableSchema(out);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A copy of `schema` with every type union rewritten as `anyOf`. */
|
|
51
|
+
export function portableSchema(schema: unknown): Schema {
|
|
52
|
+
if (!isSchema(schema)) return schema as Schema;
|
|
53
|
+
const out: Schema = {};
|
|
54
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
55
|
+
if (key === "properties" && isSchema(value)) {
|
|
56
|
+
out.properties = Object.fromEntries(
|
|
57
|
+
Object.entries(value).map(([name, sub]) => [name, portableSchema(sub)]),
|
|
58
|
+
);
|
|
59
|
+
} else if (
|
|
60
|
+
(key === "items" || key === "additionalProperties") &&
|
|
61
|
+
isSchema(value)
|
|
62
|
+
) {
|
|
63
|
+
out[key] = portableSchema(value);
|
|
64
|
+
} else if ((key === "anyOf" || key === "oneOf") && Array.isArray(value)) {
|
|
65
|
+
out[key] = value.map(portableSchema);
|
|
66
|
+
} else {
|
|
67
|
+
out[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(out.type)) {
|
|
71
|
+
const types = out.type as string[];
|
|
72
|
+
const rest: Schema = {};
|
|
73
|
+
for (const [key, value] of Object.entries(out))
|
|
74
|
+
if (key !== "type" && !ALL_BRANCH_KEYWORDS.has(key)) rest[key] = value;
|
|
75
|
+
return { ...rest, anyOf: types.map((type) => branch(type, out)) };
|
|
76
|
+
}
|
|
77
|
+
if (out.type === "array" && out.items === undefined)
|
|
78
|
+
out.items = ANY_SCALAR_ITEMS;
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../audio/preview.ts";
|
|
24
24
|
import { renderScorePcm, type RenderedAudio } from "../audio/wav.ts";
|
|
25
25
|
import type { ScoreMeasurement } from "../audio/measure.ts";
|
|
26
|
+
import { VOICE_PREVIEWABLE_TOOLS } from "./voice-tools.ts";
|
|
26
27
|
import type {
|
|
27
28
|
ActionContext,
|
|
28
29
|
AgentTool,
|
|
@@ -48,6 +49,8 @@ export const PREVIEWABLE_TOOLS: readonly string[] = Object.freeze([
|
|
|
48
49
|
"set_granular",
|
|
49
50
|
"set_drum_kit",
|
|
50
51
|
"use_sound",
|
|
52
|
+
// 0.7 Voice: flagged per lane in voice-tools.ts.
|
|
53
|
+
...VOICE_PREVIEWABLE_TOOLS,
|
|
51
54
|
]);
|
|
52
55
|
|
|
53
56
|
const MAX_CHANGES = 8;
|
|
@@ -160,7 +163,7 @@ export function previewSoundTool(
|
|
|
160
163
|
return {
|
|
161
164
|
name: "preview_sound",
|
|
162
165
|
description:
|
|
163
|
-
"Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample,
|
|
166
|
+
"Hear a track, or a candidate sound change, without committing it. changes: sound tool calls to try ({tool, args} for set_fx, set_rig, set_synth, set_string, set_modal, set_wind, set_wavetable, set_instrument, set_sample, fit_sample, set_mix, set_automation, set_drum_kit, use_sound), applied to a copy. Renders the track's notes over up to 4 bars (or a short phrase by role when it has none), solo or in context, and returns RMS/peak dBFS, spectral centroid and a one-line description for the current and the candidate sound. When the user's window is open it plays the snippet once. Then commit with the normal tools if it sounds right.",
|
|
164
167
|
parameters: {
|
|
165
168
|
type: "object",
|
|
166
169
|
properties: {
|
package/src/agent/provider.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runCommandAgentTurn } from "./command-agent.ts";
|
|
1
2
|
import { statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import {
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
} from "./gateway.ts";
|
|
30
31
|
import {
|
|
31
32
|
defaultModelId,
|
|
33
|
+
fallbackModelId,
|
|
32
34
|
MODEL_CATALOG,
|
|
33
35
|
modelAlias,
|
|
34
36
|
resolveModelChoice,
|
|
@@ -90,7 +92,7 @@ export function isApiSelection(
|
|
|
90
92
|
return selection.kind === "gateway" || selection.kind === "openrouter";
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export const LOGIN_HINT = "run `dawg
|
|
95
|
+
export const LOGIN_HINT = "run `dawg model key` to enable the agent";
|
|
94
96
|
/** The small, cheap model used for one-line helpers such as session names. */
|
|
95
97
|
export const GATEWAY_SMALL_MODEL = "anthropic/claude-haiku-4.5";
|
|
96
98
|
export const OPENROUTER_SMALL_MODEL = "anthropic/claude-haiku-4.5";
|
|
@@ -113,7 +115,7 @@ export function providerChoice(
|
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
117
|
* The model for a key-based provider: `DAWG_MODEL`, then the model saved by
|
|
116
|
-
* `/model` or `dawg
|
|
118
|
+
* `/model` or `dawg model key`, then Opus 5.5. Throws on an unknown `DAWG_MODEL`.
|
|
117
119
|
*/
|
|
118
120
|
export function apiModelId(
|
|
119
121
|
service: ApiProvider,
|
|
@@ -186,14 +188,14 @@ export async function selectProvider(
|
|
|
186
188
|
}
|
|
187
189
|
if (choice === service)
|
|
188
190
|
return offline(
|
|
189
|
-
`no ${service === "gateway" ? "AI Gateway" : "OpenRouter"} key any more; run \`dawg
|
|
191
|
+
`no ${service === "gateway" ? "AI Gateway" : "OpenRouter"} key any more; run \`dawg model key ${service}\``,
|
|
190
192
|
);
|
|
191
193
|
}
|
|
192
194
|
const bin = resolveXcbBin(auth.env, auth.runner);
|
|
193
195
|
if (!bin)
|
|
194
196
|
return offline(
|
|
195
197
|
explicit
|
|
196
|
-
? `xcb is not installed; run \`dawg
|
|
198
|
+
? `xcb is not installed; run \`dawg model key ${choice === "xcb" ? "--xcb" : choice}\``
|
|
197
199
|
: `no model configured; ${LOGIN_HINT}`,
|
|
198
200
|
);
|
|
199
201
|
let capabilities: XcbCapabilities;
|
|
@@ -251,7 +253,7 @@ export async function selectProvider(
|
|
|
251
253
|
"<account>",
|
|
252
254
|
account.id,
|
|
253
255
|
)
|
|
254
|
-
}; run \`dawg
|
|
256
|
+
}; run \`dawg model key ${family === "xcb" ? "--xcb" : family}\``,
|
|
255
257
|
);
|
|
256
258
|
}
|
|
257
259
|
// No saved pick: the first usable account, ready before admission pending.
|
|
@@ -274,7 +276,7 @@ export async function selectProvider(
|
|
|
274
276
|
}
|
|
275
277
|
return offline(
|
|
276
278
|
explicit
|
|
277
|
-
? `no ${FAMILY_NAMES[choice] ?? choice} is ready; run \`dawg
|
|
279
|
+
? `no ${FAMILY_NAMES[choice] ?? choice} is ready; run \`dawg model key ${choice === "xcb" ? "--xcb" : choice}\``
|
|
278
280
|
: `no model configured; ${LOGIN_HINT}`,
|
|
279
281
|
);
|
|
280
282
|
}
|
|
@@ -349,12 +351,24 @@ export async function runProviderTurn(
|
|
|
349
351
|
...(options.signal ? { signal: options.signal } : {}),
|
|
350
352
|
...(options.budget ? { budget: options.budget } : {}),
|
|
351
353
|
};
|
|
352
|
-
if (isApiSelection(selection))
|
|
354
|
+
if (isApiSelection(selection)) {
|
|
355
|
+
const model = options.model ?? selection.modelId;
|
|
356
|
+
const fallback = fallbackModelId(selection.kind, model);
|
|
357
|
+
if (options.host.commands && !options.model)
|
|
358
|
+
return runCommandAgentTurn({
|
|
359
|
+
...common,
|
|
360
|
+
model,
|
|
361
|
+
...(fallback ? { fallbackModel: fallback } : {}),
|
|
362
|
+
client: options.gatewayClient ?? apiClient(selection),
|
|
363
|
+
commands: options.host.commands,
|
|
364
|
+
});
|
|
353
365
|
return runAgentTurn({
|
|
354
366
|
...common,
|
|
355
|
-
model
|
|
367
|
+
model,
|
|
368
|
+
...(fallback ? { fallbackModel: fallback } : {}),
|
|
356
369
|
client: options.gatewayClient ?? apiClient(selection),
|
|
357
370
|
});
|
|
371
|
+
}
|
|
358
372
|
if (selection.kind === "xcb") {
|
|
359
373
|
const runner = options.runner ?? systemRunner;
|
|
360
374
|
return runTextAgentTurn({
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool for range edits (op1-ux §6.4): `edit_range` builds the same
|
|
3
|
+
* line the prompt runs (`loop 5-6`, `copy bass 5-6 to 7 x2`, `bars insert
|
|
4
|
+
* 2 at 3`, `section split chorus at 13`) and applies it through
|
|
5
|
+
* src/commands/range.ts or src/commands/arrange.ts, so the tool, the typed
|
|
6
|
+
* command and the menu agree. Its operations are the score diff.
|
|
7
|
+
*
|
|
8
|
+
* Bars count from 1, like the prompt and the bar ruler.
|
|
9
|
+
*/
|
|
10
|
+
import { diffScores } from "../../core/diff.ts";
|
|
11
|
+
import { SCORE_LIMITS, type TrackScore } from "../../core/score.ts";
|
|
12
|
+
import {
|
|
13
|
+
applySectionCommand,
|
|
14
|
+
loopSpan,
|
|
15
|
+
parseSectionCommand,
|
|
16
|
+
} from "../commands/arrange.ts";
|
|
17
|
+
import { parseLoopCommand } from "../commands/grammar.ts";
|
|
18
|
+
import { applyRangeCommand, parseRangeCommand } from "../commands/range.ts";
|
|
19
|
+
import type { AgentTool, ToolContext, ToolPlan } from "./tools.ts";
|
|
20
|
+
import { ToolArgumentError } from "./tool-error.ts";
|
|
21
|
+
|
|
22
|
+
export const RANGE_ACTIONS = [
|
|
23
|
+
"loop",
|
|
24
|
+
"unloop",
|
|
25
|
+
"copy",
|
|
26
|
+
"move",
|
|
27
|
+
"clear",
|
|
28
|
+
"reverse",
|
|
29
|
+
"insert_bars",
|
|
30
|
+
"remove_bars",
|
|
31
|
+
"split",
|
|
32
|
+
"join",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
type RangeAction = (typeof RANGE_ACTIONS)[number];
|
|
36
|
+
|
|
37
|
+
const bar = (description: string) =>
|
|
38
|
+
({
|
|
39
|
+
type: "integer",
|
|
40
|
+
minimum: 1,
|
|
41
|
+
maximum: SCORE_LIMITS.maxBars,
|
|
42
|
+
description,
|
|
43
|
+
}) as const;
|
|
44
|
+
|
|
45
|
+
function whole(value: unknown, what: string, max: number): number {
|
|
46
|
+
if (
|
|
47
|
+
typeof value !== "number" ||
|
|
48
|
+
!Number.isInteger(value) ||
|
|
49
|
+
value < 1 ||
|
|
50
|
+
value > max
|
|
51
|
+
)
|
|
52
|
+
throw new ToolArgumentError(`${what} must be a whole number 1..${max}`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function word(value: unknown, what: string): string {
|
|
57
|
+
if (typeof value !== "string" || !/^[\w .'-]{1,64}$/u.test(value.trim()))
|
|
58
|
+
throw new ToolArgumentError(`${what} must be a name`);
|
|
59
|
+
return value.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `5-6`, `5`, or a section name: the range words of the typed command. */
|
|
63
|
+
function rangeWords(args: Record<string, unknown>): string {
|
|
64
|
+
if (args.section !== undefined) return word(args.section, "section");
|
|
65
|
+
if (args.fromBar === undefined)
|
|
66
|
+
throw new ToolArgumentError("give fromBar (and toBar) or section");
|
|
67
|
+
const from = whole(args.fromBar, "fromBar", SCORE_LIMITS.maxBars);
|
|
68
|
+
const to =
|
|
69
|
+
args.toBar === undefined
|
|
70
|
+
? from
|
|
71
|
+
: whole(args.toBar, "toBar", SCORE_LIMITS.maxBars);
|
|
72
|
+
if (to < from) throw new ToolArgumentError("toBar comes at or after fromBar");
|
|
73
|
+
return from === to ? `${from}` : `${from}-${to}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function trackWord(
|
|
77
|
+
args: Record<string, unknown>,
|
|
78
|
+
context: ToolContext,
|
|
79
|
+
): string {
|
|
80
|
+
if (args.allTracks === true) return "all";
|
|
81
|
+
const id =
|
|
82
|
+
args.trackId === undefined
|
|
83
|
+
? context.focusedTrackId
|
|
84
|
+
: word(args.trackId, "trackId");
|
|
85
|
+
if (!context.score.tracks.some((track) => track.id === id))
|
|
86
|
+
throw new ToolArgumentError(`no track ${id}`);
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The typed command a call stands for (what show-me would type). */
|
|
91
|
+
export function rangeToolCommand(
|
|
92
|
+
args: Record<string, unknown>,
|
|
93
|
+
context: ToolContext,
|
|
94
|
+
): string {
|
|
95
|
+
const action = args.action as RangeAction;
|
|
96
|
+
if (!RANGE_ACTIONS.includes(action))
|
|
97
|
+
throw new ToolArgumentError(`action must be ${RANGE_ACTIONS.join(", ")}`);
|
|
98
|
+
switch (action) {
|
|
99
|
+
case "loop":
|
|
100
|
+
return `loop ${rangeWords(args)}`;
|
|
101
|
+
case "unloop":
|
|
102
|
+
return "loop off";
|
|
103
|
+
case "copy":
|
|
104
|
+
case "move": {
|
|
105
|
+
if (args.toBar === undefined && args.atBar === undefined)
|
|
106
|
+
throw new ToolArgumentError(`${action} needs atBar, where it lands`);
|
|
107
|
+
const at = whole(args.atBar, "atBar", SCORE_LIMITS.maxBars);
|
|
108
|
+
const times =
|
|
109
|
+
action === "copy" && args.times !== undefined
|
|
110
|
+
? ` x${whole(args.times, "times", 64)}`
|
|
111
|
+
: "";
|
|
112
|
+
const mode =
|
|
113
|
+
args.insert === true
|
|
114
|
+
? " insert"
|
|
115
|
+
: action === "copy" && args.merge === true
|
|
116
|
+
? " merge"
|
|
117
|
+
: "";
|
|
118
|
+
return `${action} ${trackWord(args, context)} ${rangeWords(args)} to ${at}${times}${mode}`;
|
|
119
|
+
}
|
|
120
|
+
case "clear":
|
|
121
|
+
case "reverse":
|
|
122
|
+
return `${action} ${trackWord(args, context)} ${rangeWords(args)}`;
|
|
123
|
+
case "insert_bars":
|
|
124
|
+
return `bars insert ${whole(args.bars, "bars", SCORE_LIMITS.maxBars)} at ${whole(args.atBar, "atBar", SCORE_LIMITS.maxBars)}`;
|
|
125
|
+
case "remove_bars":
|
|
126
|
+
return `bars remove ${rangeWords(args)}`;
|
|
127
|
+
case "split":
|
|
128
|
+
return `section split ${word(args.section, "section")} at ${whole(args.atBar, "atBar", SCORE_LIMITS.maxBars)}`;
|
|
129
|
+
case "join":
|
|
130
|
+
return `section join ${word(args.section, "section")}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function run(
|
|
135
|
+
line: string,
|
|
136
|
+
context: ToolContext,
|
|
137
|
+
): { next?: TrackScore; message: string } {
|
|
138
|
+
const score = context.score;
|
|
139
|
+
const loop = parseLoopCommand(line);
|
|
140
|
+
if (loop?.type === "loop-bars") {
|
|
141
|
+
const result = loopSpan(score, loop.from, loop.to);
|
|
142
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
143
|
+
return { next: result.next, message: result.message };
|
|
144
|
+
}
|
|
145
|
+
if (loop?.type === "loop-off")
|
|
146
|
+
return score.loop
|
|
147
|
+
? { next: score.withLoop(null), message: "loop · off · playing the song" }
|
|
148
|
+
: run("section loop off", context);
|
|
149
|
+
if (loop?.type === "loop-section")
|
|
150
|
+
return run(`section loop ${loop.name}`, context);
|
|
151
|
+
const range = parseRangeCommand(line, score);
|
|
152
|
+
if (range) {
|
|
153
|
+
const result = applyRangeCommand(
|
|
154
|
+
score,
|
|
155
|
+
{ trackId: context.focusedTrackId, playheadBar: 0 },
|
|
156
|
+
range,
|
|
157
|
+
);
|
|
158
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
159
|
+
return {
|
|
160
|
+
...(result.next ? { next: result.next } : {}),
|
|
161
|
+
message: result.message,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const section = parseSectionCommand(line, score);
|
|
165
|
+
if (!section) throw new ToolArgumentError(`cannot run: ${line}`);
|
|
166
|
+
const result = applySectionCommand(score, context.focusedTrackId, section);
|
|
167
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
168
|
+
return {
|
|
169
|
+
...(result.next ? { next: result.next } : {}),
|
|
170
|
+
message: result.message,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const RANGE_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
175
|
+
{
|
|
176
|
+
name: "edit_range",
|
|
177
|
+
description:
|
|
178
|
+
"Edit bar ranges (bars count from 1; a range is fromBar..toBar or a section name). loop sets the loop range playback cycles (no section is made); unloop plays the song. copy copies a track's (or allTracks') notes, automation and clips in the range to atBar, overwriting there (merge keeps what is there; insert shifts later music right), times tiles it end to end. move is copy plus clearing the source. clear empties the range (the bars stay). reverse mirrors it in time. insert_bars adds `bars` empty bars at atBar and moves later sections, clips, automation and tempo points right; remove_bars cuts the range out. split cuts a section in two at atBar; join merges a section with the one after it. Each call is one undo step and runs the same command the prompt does.",
|
|
179
|
+
parameters: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
action: { type: "string", enum: [...RANGE_ACTIONS] },
|
|
183
|
+
fromBar: bar("first bar of the range"),
|
|
184
|
+
toBar: bar("last bar of the range (default fromBar)"),
|
|
185
|
+
section: {
|
|
186
|
+
type: "string",
|
|
187
|
+
maxLength: 32,
|
|
188
|
+
description: "a section as the range (split/join: the section)",
|
|
189
|
+
},
|
|
190
|
+
atBar: bar("copy/move: where it lands; insert_bars/split: the bar"),
|
|
191
|
+
bars: bar("insert_bars: how many"),
|
|
192
|
+
times: { type: "integer", minimum: 1, maximum: 64 },
|
|
193
|
+
trackId: {
|
|
194
|
+
type: "string",
|
|
195
|
+
maxLength: SCORE_LIMITS.maxIdLength,
|
|
196
|
+
description: "the track (default: the focused track)",
|
|
197
|
+
},
|
|
198
|
+
allTracks: { type: "boolean" },
|
|
199
|
+
insert: { type: "boolean" },
|
|
200
|
+
merge: { type: "boolean" },
|
|
201
|
+
},
|
|
202
|
+
required: ["action"],
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
},
|
|
205
|
+
plan(args, context): ToolPlan {
|
|
206
|
+
const line = rangeToolCommand(args, context);
|
|
207
|
+
const result = run(line, context);
|
|
208
|
+
return {
|
|
209
|
+
kind: "score",
|
|
210
|
+
operations: result.next ? diffScores(context.score, result.next) : [],
|
|
211
|
+
trackId: context.focusedTrackId,
|
|
212
|
+
summary: result.message,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
@@ -75,7 +75,7 @@ export const RHYTHM_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
75
75
|
{
|
|
76
76
|
name: "set_rhythm",
|
|
77
77
|
description:
|
|
78
|
-
"Preferred for drums: one row per voice of a kit/oneshot track; its notes regenerate from the row. pulses (4) over steps (16..64) Euclidean, rotate later; division 1/32..1/1 (1/16); grid 'x.X.' explicit (X accent); repeats 0..16 after each pulse every time, pace -1..1, ramp -1..1; velocity (0.8); accent 0..1 on E(accents,pulses); gate; legato; probability+seed; swing/nudge ±0.5 step; cycles [{pulses,rotate,repeats,probability,velocity}] per pass. remove [voices] (freeze keeps notes).",
|
|
78
|
+
"Preferred for drums: one row per voice of a kit/oneshot track; its notes regenerate from the row. pulses (4) over steps (16..64) Euclidean, rotate later; division 1/32..1/1 (1/16) is one step, and the steps×division cycle repeats from beat 0 to fill the loop (E(4,16) at 1/16 = a hit every beat, four on the floor; a backbeat on counts 2 and 4 is grid '....x.......x...' or E(2,16) rotate 4; offbeat 'and's are grid '..x.' ); grid 'x.X.' explicit (X accent); repeats 0..16 after each pulse every time, pace -1..1, ramp -1..1; velocity (0.8); accent 0..1 on E(accents,pulses); gate; legato; probability+seed; swing/nudge ±0.5 step; cycles [{pulses,rotate,repeats,probability,velocity}] per pass. remove [voices] (freeze keeps notes).",
|
|
79
79
|
parameters: {
|
|
80
80
|
type: "object",
|
|
81
81
|
properties: {
|
|
@@ -380,7 +380,7 @@ export const SECTION_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
382
|
name: "add_transition",
|
|
383
|
-
description: `Generate a transition with the built-in voices (bars count from 1). build: noise riser, accelerating snare roll, filter sweep and uplifter (all on by default), landing on the bar after its range. The range is \`bars\` bars (default 4) just before section \`into\` (the usual build into a drop or chorus); or a section's own bars (its last \`bars\` bars when bars is given); or \`bars\` bars starting at atBar; default the song's last 4 bars. The sweep is a high-pass
|
|
383
|
+
description: `Generate a transition with the built-in voices (bars count from 1). build: noise riser, accelerating snare roll, filter sweep and uplifter (all on by default), landing on the bar after its range. The range is \`bars\` bars (default 4) just before section \`into\` (the usual build into a drop or chorus); or a section's own bars (its last \`bars\` bars when bars is given); or \`bars\` bars starting at atBar; default the song's last 4 bars. The sweep is a high-pass rising to 1.2 kHz on pitched tracks (a low-pass opening from cutoff/10 on low-pass tracks), snapping back on the downbeat. drop: a pre-drop cut (cutBeats of silence, default 1, up to two bars) and an impact hit on the section's first bar or atBar (default: the section named drop, else chorus). fill: a drum fill (${FILL_STYLES.join(", ")}) in the last beats (default 1, ${FILL_BEATS_MIN} up to two bars) leading into the section or into atBar, plus a crash on its downbeat. Without section or atBar, fills go at every section boundary. Out-of-range values are clamped.`,
|
|
384
384
|
parameters: {
|
|
385
385
|
type: "object",
|
|
386
386
|
properties: {
|