updated skills
This commit is contained in:
@@ -34,10 +34,10 @@ import { AudioDocument } from "@suaveplan/audio-core";
|
||||
import { BeatDetector } from "@suaveplan/audio-analysis-beat";
|
||||
|
||||
const doc = new AudioDocument({
|
||||
sampleRate: 48_000,
|
||||
channels: 1,
|
||||
sourceLength: pcm.length,
|
||||
pages: [{ channelData: [pcm], startSample: 0 }],
|
||||
sampleRate: 48_000,
|
||||
channels: 1,
|
||||
sourceLength: pcm.length,
|
||||
pages: [{ channelData: [pcm], startSample: 0 }],
|
||||
});
|
||||
|
||||
const result = await new BeatDetector().analyze(doc);
|
||||
@@ -65,4 +65,3 @@ console.log(result.bpm, result.beats.length, result.confidence);
|
||||
|
||||
See per-module `.md` files for the deep documentation of each algorithm and
|
||||
entry point.
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ gives a confidence near 1, scattered onsets give one near 0.
|
||||
import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-grid";
|
||||
|
||||
const result = buildBeatGrid({
|
||||
period: 60 / 120, // 120 BPM
|
||||
duration: 10,
|
||||
onsets: [0.5, 1.0, 1.5, 2.0],
|
||||
period: 60 / 120, // 120 BPM
|
||||
duration: 10,
|
||||
onsets: [0.5, 1.0, 1.5, 2.0],
|
||||
});
|
||||
console.log(result.beats.length, result.phase, result.confidence);
|
||||
```
|
||||
@@ -46,7 +46,7 @@ import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-gr
|
||||
// more tightly — this is how the BeatDetector breaks ties between candidate
|
||||
// tempi when an autocorrelation sweep returns two near-equal peaks.
|
||||
const onsets = new Float64Array([
|
||||
0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
|
||||
0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
|
||||
]);
|
||||
|
||||
const a = buildBeatGrid({ period: 60 / 120, duration: 4.2, onsets, phaseTrials: 48 });
|
||||
|
||||
@@ -29,9 +29,9 @@ import { combTempo } from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo"
|
||||
|
||||
// ODF sampled every 1024 samples at 48 kHz → hopSeconds = 1024/48000.
|
||||
const result = combTempo(odf, {
|
||||
minBpm: 60,
|
||||
maxBpm: 200,
|
||||
hopSeconds: 1024 / 48_000,
|
||||
minBpm: 60,
|
||||
maxBpm: 200,
|
||||
hopSeconds: 1024 / 48_000,
|
||||
});
|
||||
console.log(result.bpm); // e.g. 120
|
||||
```
|
||||
@@ -40,8 +40,8 @@ console.log(result.bpm); // e.g. 120
|
||||
|
||||
```ts
|
||||
import {
|
||||
combScore,
|
||||
combTempo,
|
||||
combScore,
|
||||
combTempo,
|
||||
} from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo";
|
||||
|
||||
// Draw a tempo-confidence curve over the search range manually:
|
||||
@@ -50,8 +50,8 @@ const min = 60;
|
||||
const max = 200;
|
||||
const curve = new Float64Array(max - min + 1);
|
||||
for (let bpm = min; bpm <= max; bpm++) {
|
||||
const period = Math.max(1, Math.round((60 / bpm) / hopSeconds));
|
||||
curve[bpm - min] = combScore(odf, period);
|
||||
const period = Math.max(1, Math.round((60 / bpm) / hopSeconds));
|
||||
curve[bpm - min] = combScore(odf, period);
|
||||
}
|
||||
|
||||
// Fall back to combTempo for the canonical winner including octave handling.
|
||||
|
||||
@@ -37,8 +37,8 @@ const odf = energyOdf(energies);
|
||||
|
||||
```ts
|
||||
import {
|
||||
blockEnergy,
|
||||
energyOdfFromSamples,
|
||||
blockEnergy,
|
||||
energyOdfFromSamples,
|
||||
} from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
|
||||
|
||||
const sampleRate = 48_000;
|
||||
@@ -53,10 +53,10 @@ const hopSeconds = blockSize / sampleRate;
|
||||
let bestIdx = 0;
|
||||
let best = 0;
|
||||
for (let i = 0; i < Math.floor(1 / hopSeconds); i++) {
|
||||
if (odf[i] > best) {
|
||||
best = odf[i];
|
||||
bestIdx = i;
|
||||
}
|
||||
if (odf[i] > best) {
|
||||
best = odf[i];
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
console.log("onset at", bestIdx * hopSeconds, "seconds");
|
||||
console.log("avg first-block energy", blockEnergy([left.subarray(0, blockSize)]));
|
||||
|
||||
@@ -41,8 +41,8 @@ console.log(odf.length, "frames"); // ceil(192000 / 512) = 375
|
||||
|
||||
```ts
|
||||
import {
|
||||
magnitudeSpectrum,
|
||||
spectralFlux,
|
||||
magnitudeSpectrum,
|
||||
spectralFlux,
|
||||
} from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
|
||||
|
||||
const fftSize = 2048;
|
||||
@@ -55,8 +55,8 @@ const mono = new Float32Array(48_000 * 10); // 10 seconds
|
||||
// thread per-frame work through other code, or thin the ODF on the fly.
|
||||
const odfValues: number[] = [];
|
||||
for (let start = 0; start + fftSize <= mono.length; start += hop) {
|
||||
buf.set(mono.subarray(start, start + fftSize));
|
||||
odfValues.push(spectralFlux(buf, prev, fftSize));
|
||||
buf.set(mono.subarray(start, start + fftSize));
|
||||
odfValues.push(spectralFlux(buf, prev, fftSize));
|
||||
}
|
||||
|
||||
// Verify the magnitude spectrum has the expected shape on its own.
|
||||
|
||||
@@ -42,10 +42,10 @@ import { AudioDocument } from "@suaveplan/audio-core";
|
||||
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
|
||||
|
||||
const doc = new AudioDocument({
|
||||
sampleRate: 48_000,
|
||||
channels: 1,
|
||||
sourceLength: 48_000 * 4,
|
||||
pages: [{ startSample: 0, channelData: [Array.from(/* 4 s mono */)] }],
|
||||
sampleRate: 48_000,
|
||||
channels: 1,
|
||||
sourceLength: 48_000 * 4,
|
||||
pages: [{ startSample: 0, channelData: [Array.from(/* 4 s mono */)] }],
|
||||
});
|
||||
|
||||
const detector = new BeatDetector();
|
||||
@@ -60,11 +60,11 @@ import { AudioDocument } from "@suaveplan/audio-core";
|
||||
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
|
||||
|
||||
const detector = new BeatDetector({
|
||||
algorithm: "spectral-flux",
|
||||
minBpm: 80,
|
||||
maxBpm: 160,
|
||||
fftSize: 2048,
|
||||
hopSize: 1024,
|
||||
algorithm: "spectral-flux",
|
||||
minBpm: 80,
|
||||
maxBpm: 160,
|
||||
fftSize: 2048,
|
||||
hopSize: 1024,
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
@@ -73,10 +73,10 @@ setTimeout(() => controller.abort(), 5_000);
|
||||
// Streaming refinement
|
||||
let final;
|
||||
for await (const partial of detector.analyzeStream(doc, {
|
||||
signal: controller.signal,
|
||||
signal: controller.signal,
|
||||
})) {
|
||||
final = partial;
|
||||
console.log("BPM so far:", partial.bpm, partial.confidence);
|
||||
final = partial;
|
||||
console.log("BPM so far:", partial.bpm, partial.confidence);
|
||||
}
|
||||
|
||||
// Or drive it through the audio-core AnalysisOp surface
|
||||
|
||||
@@ -34,13 +34,13 @@ right log level without re-deriving it.
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_ANALYSIS_BEAT_CODES,
|
||||
AudioAnalysisBeatError,
|
||||
AUDIO_ANALYSIS_BEAT_CODES,
|
||||
AudioAnalysisBeatError,
|
||||
} from "@suaveplan/audio-analysis-beat/error-codes";
|
||||
|
||||
throw new AudioAnalysisBeatError(
|
||||
AUDIO_ANALYSIS_BEAT_CODES.EMPTY_DOCUMENT,
|
||||
"audio-analysis-beat.empty-document",
|
||||
AUDIO_ANALYSIS_BEAT_CODES.EMPTY_DOCUMENT,
|
||||
"audio-analysis-beat.empty-document",
|
||||
);
|
||||
```
|
||||
|
||||
@@ -48,21 +48,21 @@ throw new AudioAnalysisBeatError(
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_ANALYSIS_BEAT_CODES,
|
||||
AUDIO_ANALYSIS_BEAT_ERROR_METADATA,
|
||||
AudioAnalysisBeatError,
|
||||
AUDIO_ANALYSIS_BEAT_CODES,
|
||||
AUDIO_ANALYSIS_BEAT_ERROR_METADATA,
|
||||
AudioAnalysisBeatError,
|
||||
} from "@suaveplan/audio-analysis-beat/error-codes";
|
||||
|
||||
function logError(err: unknown): void {
|
||||
if (!(err instanceof AudioAnalysisBeatError)) throw err;
|
||||
const meta = AUDIO_ANALYSIS_BEAT_ERROR_METADATA[err.code];
|
||||
console[meta.severity === "warning" ? "warn" : "error"](
|
||||
`[${err.category}] ${err.code}: ${meta.description}`,
|
||||
{ context: err.context, cause: err.cause },
|
||||
);
|
||||
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.ABORTED) return;
|
||||
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.NO_BEATS_DETECTED) return;
|
||||
throw err;
|
||||
if (!(err instanceof AudioAnalysisBeatError)) throw err;
|
||||
const meta = AUDIO_ANALYSIS_BEAT_ERROR_METADATA[err.code];
|
||||
console[meta.severity === "warning" ? "warn" : "error"](
|
||||
`[${err.category}] ${err.code}: ${meta.description}`,
|
||||
{ context: err.context, cause: err.cause },
|
||||
);
|
||||
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.ABORTED) return;
|
||||
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.NO_BEATS_DETECTED) return;
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -53,4 +53,3 @@ console.log({
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -60,4 +60,3 @@ All errors are `AudioAnalysisError`s carrying one of the `AUDIO_ANALYSIS_PITCH.*
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -59,4 +59,3 @@ console.log(result.centroid, result.rolloff, result.flux);
|
||||
## Status
|
||||
|
||||
`0.1.0`. Pre-publish gate (lint + typecheck + test) and 100% coverage are required on every change.
|
||||
|
||||
|
||||
@@ -101,4 +101,3 @@ Stable mapping from `AUDIO_CLI.*` codes onto Unix exit codes:
|
||||
- 100/100/100/100 coverage on every implementation file.
|
||||
- Zero mocks — tests drive the real CLI against real `mkdtemp` temp files
|
||||
and the real `@suaveplan/audio-engine-server` adapters.
|
||||
|
||||
|
||||
@@ -78,4 +78,3 @@ Eight codes namespaced under `AUDIO_CODEC_FFMPEG_NATIVE.*`:
|
||||
`0.1.0` — first-tier landing. Subpath exports cover every implementation
|
||||
module so consumers can tree-shake. See the package's OpenSpec change for
|
||||
the full specification.
|
||||
|
||||
|
||||
@@ -124,4 +124,3 @@ For testing without a real worker, supply:
|
||||
These are dependency-injection seams, not mocks; production callers
|
||||
omit them and the real `new Worker(coreUrl)` / dynamic `import()` path
|
||||
runs unchanged.
|
||||
|
||||
|
||||
@@ -69,10 +69,10 @@ modules.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The COI check intentionally happens *before* the dynamic `import()` so
|
||||
The COI check intentionally happens _before_ the dynamic `import()` so
|
||||
that callers without the `@ffmpeg/core-mt` optional dep installed never
|
||||
incur a module-not-found warning when the COI gate is the real failure.
|
||||
The fallback path runs only when COI is true *and* the mt import rejects
|
||||
The fallback path runs only when COI is true _and_ the mt import rejects
|
||||
— if COI is true and mt loads successfully, no fallback occurs. The
|
||||
`crossOriginIsolated` override exists so tests can drive both branches
|
||||
deterministically without manipulating the global.
|
||||
|
||||
@@ -7,7 +7,7 @@ _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
This module owns the pure logic of the encode pipeline: validating the
|
||||
requested format against the supported set *before* the worker is touched,
|
||||
requested format against the supported set _before_ the worker is touched,
|
||||
interleaving per-channel PCM into a single `Float32Array`, projecting
|
||||
that into ffmpeg's `f32le` wire format, building the format-specific
|
||||
ffmpeg command for each of `wav` / `mp3` / `ogg` / `opus` / `flac` /
|
||||
|
||||
@@ -100,7 +100,7 @@ if (fmt === "caf") {
|
||||
- The format ID at offset 8 of `desc` is `lpcm` (`0x6c70636d`) for
|
||||
uncompressed integer PCM. The decoder does not currently dispatch on
|
||||
this field — it assumes `lpcm` and decodes accordingly. Other format
|
||||
IDs (`alac`, `aac `, `lpcm` floats) would produce wrong output;
|
||||
IDs (`alac`, `aac`, `lpcm` floats) would produce wrong output;
|
||||
callers needing compressed CAF support should use the corresponding
|
||||
WASM codec for the inner format.
|
||||
- Sample byte order is determined per sample by the
|
||||
|
||||
@@ -24,7 +24,7 @@ storage format (PCM int vs IEEE float).
|
||||
- `WavCodec.canDecode(bytes)` — magic-byte sniff: returns `true` iff the
|
||||
buffer is at least 12 bytes and starts with `RIFF` … `WAVE`.
|
||||
- `WavCodec.decode(bytes)` — parses the RIFF header, walks the chunk list
|
||||
for `fmt ` and `data`, decodes the PCM, and returns a `DecodeResult`
|
||||
for `fmt` and `data`, decodes the PCM, and returns a `DecodeResult`
|
||||
with `channelData`, `sampleRate`, and `format: "wav"`.
|
||||
- `WavCodec.encode(channelData, opts?)` — emits a complete RIFF WAV from
|
||||
PCM channels at the requested `sampleRate`, `bitDepth`, and
|
||||
@@ -106,7 +106,7 @@ encoder.close();
|
||||
- `canDecode(bytes: Uint8Array): boolean`
|
||||
- `decode(bytes: Uint8Array, opts?: DecodeOpts): Promise<DecodeResult>` —
|
||||
throws `AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` on bad magic,
|
||||
missing `fmt ` chunk, missing `data` chunk, truncated `data`, fmt size
|
||||
missing `fmt` chunk, missing `data` chunk, truncated `data`, fmt size
|
||||
< 16, or unsupported `bitsPerSample` (`bps === 0` or `bps > 4`).
|
||||
- `encode(channelData, opts?: EncodeOpts): Promise<Uint8Array>` — throws
|
||||
`AudioCodecsError("AUDIO_CODECS.ENCODE_FAILED")` if `channelData` is
|
||||
@@ -120,7 +120,7 @@ encoder.close();
|
||||
## Implementation Notes
|
||||
|
||||
- The RIFF chunk walker tolerates and skips any chunk whose ID is neither
|
||||
`fmt ` nor `data` — `LIST`, `bext`, `iXML`, and other metadata chunks
|
||||
`fmt` nor `data` — `LIST`, `bext`, `iXML`, and other metadata chunks
|
||||
are passed over silently. This matches the behaviour every real WAV
|
||||
consumer expects.
|
||||
- 24-bit PCM uses three signed-magnitude bytes per sample in
|
||||
|
||||
@@ -72,4 +72,3 @@ and `bindParameter` integration with the FX plugin family.
|
||||
## Status
|
||||
|
||||
`0.1.0` — Wave 1 migration. 100/100/100/100 coverage.
|
||||
|
||||
|
||||
@@ -98,4 +98,3 @@ for await (const block of doc.stream()) {
|
||||
- [`src/stats/stats.md`](src/stats/stats.md) — `computeStats` for peak, RMS, and DC offset
|
||||
- [`src/stream/stream.md`](src/stream/stream.md) — `createPlanTransformStream` for downstream transforms
|
||||
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioError` class
|
||||
|
||||
|
||||
@@ -78,4 +78,3 @@ try {
|
||||
- Resampling — surfaced as `RESAMPLE_UNSUPPORTED`; a future `@suaveplan/audio-resample` will pair with this package.
|
||||
- Non-WAV formats — sibling `audio-decode-{mp3,flac,opus,…}` packages each wrap their own upstream decoder behind the same contract.
|
||||
- Format dispatching — owned by the upcoming `@suaveplan/audio-decode` umbrella package.
|
||||
|
||||
|
||||
@@ -111,4 +111,3 @@ adapter.dispose();
|
||||
(`audio_engine_browser_offline_render_duration_ms`).
|
||||
- 100% test coverage; tests run under `happy-dom` with an injected
|
||||
context shim.
|
||||
|
||||
|
||||
@@ -69,4 +69,3 @@ exporter is registered and has no import-time side effects.
|
||||
|
||||
See the specification for the full
|
||||
requirement set.
|
||||
|
||||
|
||||
@@ -85,4 +85,3 @@ Both gates hard-fail below 100% coverage. The Python test suite is not currently
|
||||
|
||||
- `@suaveplan/react-audio-fingerprint` — the React 19 sibling that runs the browser-tier fingerprinter and, via this package's `python-backend` integration, consults this service for high-accuracy matching.
|
||||
- `@suaveplan/audio-analysis-spectrum` — the universal FFT/spectrogram primitives the browser-tier fingerprinter is built on.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -51,4 +51,3 @@ console.log(jump.length); // encoded byte length
|
||||
- [`src/types/common.md`](src/types/common.md) — `AudioAdapter` interface, Zod schemas, and public types
|
||||
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioSpriteError` class
|
||||
- [`src/__benchmarks__/audio-sprite-packer.bench.md`](src/__benchmarks__/audio-sprite-packer.bench.md) — Benchmark suite notes
|
||||
|
||||
|
||||
@@ -77,4 +77,3 @@ See the co-located `.md` files inside `src/`:
|
||||
- [`telemetry/telemetry.md`](./src/telemetry/telemetry.md)
|
||||
- [`schemas.md`](./src/schemas.md)
|
||||
- [`error-codes.md`](./src/error-codes.md)
|
||||
|
||||
|
||||
@@ -38,4 +38,3 @@ Headless DAW timeline engine with multi-track arrangements, clips, playback, loo
|
||||
- [zoom](./src/zoom/zoom.md) — The `zoom.ts` module provides a discrete zoom table and navigation helpers for the timeline view. Zoom levels are expre…
|
||||
- [zoom](./src/zoom/zoom.md) — The `zoom.ts` module provides a discrete zoom table and navigation helpers for the timeline view. Zoom levels are expre…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
|
||||
@@ -55,4 +55,3 @@ scheduler.start();
|
||||
- [Signal](src/signal/signal.md) — tempo automation curves
|
||||
- [Time](src/time/time.md) — musical time parsing and conversion
|
||||
- [Error Codes](src/error-codes.md) — structured error registry
|
||||
|
||||
|
||||
@@ -207,4 +207,3 @@ const traktor = {
|
||||
- [Upgrade](./src/upgrade/upgrade.md)
|
||||
- [Walk](./src/walk/walk.md)
|
||||
- [Identity](./src/identity/identity.md)
|
||||
|
||||
|
||||
@@ -87,4 +87,3 @@ const messages = decoder.drain();
|
||||
## Coverage gate
|
||||
|
||||
`bunx turbo test:coverage --filter=@suaveplan/midi` enforces 100/100/100/100 (statements / branches / functions / lines).
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ _(no submodules)_
|
||||
|
||||
`parseSysEx` and `buildSysEx` are the canonical helpers for working with MIDI System Exclusive payloads — the manufacturer-defined byte streams that sit between an `F0` prefix and an `F7` terminator. Every vendor uses SysEx for patch dumps, parameter sysex, MMC (MIDI Machine Control), and identity-request handshakes; standardising the parse / build code in one place prevents off-by-one errors with the one-byte vs three-byte manufacturer-id forms.
|
||||
|
||||
`parseSysEx` expects the *body* of the frame (i.e. everything between `F0` and `F7`, exclusive of both endpoints). `buildSysEx` produces a complete `F0...F7` frame; callers feed the result straight to `MidiPortLike.send`.
|
||||
`parseSysEx` expects the _body_ of the frame (i.e. everything between `F0` and `F7`, exclusive of both endpoints). `buildSysEx` produces a complete `F0...F7` frame; callers feed the result straight to `MidiPortLike.send`.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ _(no submodules)_
|
||||
|
||||
## Purpose
|
||||
|
||||
MIDI 2.0 introduces the Universal MIDI Packet (UMP) format — fixed-width 32, 64, 96, or 128 bit packets that carry MIDI messages alongside high-resolution per-note controllers, per-note pitch, and 32-bit velocity. This module exposes the four packet types, the message-type nibble constants, and a *lossless* bridge between MIDI 1.0 messages and their UMP equivalents.
|
||||
MIDI 2.0 introduces the Universal MIDI Packet (UMP) format — fixed-width 32, 64, 96, or 128 bit packets that carry MIDI messages alongside high-resolution per-note controllers, per-note pitch, and 32-bit velocity. This module exposes the four packet types, the message-type nibble constants, and a _lossless_ bridge between MIDI 1.0 messages and their UMP equivalents.
|
||||
|
||||
The bridge is deliberately one-way for any MIDI 2.0 feature that exceeds MIDI 1.0's expressivity: a MIDI 2.0 channel-voice packet with 32-bit velocity cannot be downgraded to MIDI 1.0 without losing precision, so `umpToMidi1` rejects it with `MidiError("MIDI.UNSUPPORTED_UMP_VARIANT")` rather than silently truncating. Engines that want to "best-effort downgrade" wrap this bridge with their own approximation policy.
|
||||
|
||||
|
||||
@@ -14,4 +14,3 @@
|
||||
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
|
||||
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ WebSocket:
|
||||
consistently without per-call wiring.
|
||||
- `MultipartFile` accepts URLs as strings; the client fetches them via
|
||||
the injected `fetchImpl` before uploading. This is the only place in
|
||||
the transport that issues a *secondary* network call from a single
|
||||
the transport that issues a _secondary_ network call from a single
|
||||
caller invocation.
|
||||
- `RequestMetadata` keeps every field optional because some endpoints
|
||||
do not surface `X-RateLimit-Remaining` or `X-Character-Cost`.
|
||||
|
||||
@@ -140,4 +140,3 @@ bunx turbo test:coverage --filter=@suaveplan/voice-elevenlabs
|
||||
```
|
||||
|
||||
Coverage gate: 100/100/100/100 statements/branches/functions/lines.
|
||||
|
||||
|
||||
@@ -85,4 +85,3 @@ const result = await synth.synthesize({
|
||||
Provider-specific extensions (voice cloning, conversational AI, music,
|
||||
dubbing, voice changing) ship in their own packages under separate
|
||||
abstractions when those domains stabilise across two or more vendors.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user