Files
elizaos--eliza/plugins/plugin-elevenlabs/__tests__/streaming.test.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

587 lines
18 KiB
TypeScript

/**
* Functional streaming test for the ElevenLabs TTS plugin.
*
* The plugin's `TEXT_TO_SPEECH` model handler internally calls
* `client.textToSpeech.stream()` and drains the resulting
* `ReadableStream<Uint8Array>` via `readStreamToUint8Array`. This test
* mocks the ElevenLabs SDK so we can:
* 1. Confirm the plugin actually awaits the SDK's streaming API.
* 2. Confirm a multi-chunk stream is fully drained (truly streaming —
* the reader pulls chunk N+1 before the producer writes the last
* chunk).
* 3. Confirm the resolved bytes preserve chunk order and total size.
*/
import { describe, expect, it, vi } from "vitest";
const streamMock = vi.fn();
const convertMock = vi.fn();
const clientConfigMock = vi.fn();
// Mock the SDK before the plugin module is loaded.
vi.mock("@elevenlabs/elevenlabs-js", () => {
return {
ElevenLabsClient: class {
constructor(config: unknown) {
clientConfigMock(config);
}
textToSpeech = {
stream: streamMock,
};
speechToText = {
convert: convertMock,
};
},
};
});
vi.mock("@elevenlabs/elevenlabs-js/api", () => {
return {
SpeechToTextConvertRequestModelId: { ScribeV1: "scribe_v1" },
SpeechToTextConvertRequestTimestampsGranularity: {
None: "none",
Word: "word",
},
TextToSpeechStreamRequestOutputFormat: {
Mp3_44100_128: "mp3_44100_128",
Pcm16000: "pcm_16000",
},
};
});
interface TestRuntime {
agentId: string;
getSetting: (key: string) => string | undefined;
character: { settings: Record<string, unknown> };
}
function createTestRuntime(
settings: Record<string, string | undefined> = {},
): TestRuntime {
const merged: Record<string, string | undefined> = {
ELEVENLABS_API_KEY: "sk-test-key",
ELEVENLABS_VOICE_ID: "voice-123",
ELEVENLABS_MODEL_ID: "eleven_monolingual_v1",
ELEVENLABS_OUTPUT_FORMAT: "mp3_44100_128",
...settings,
};
return {
agentId: "test-agent",
getSetting: (key: string) => merged[key],
character: { settings: {} },
};
}
function setGlobalValue(key: string, value: unknown): () => void {
const hadValue = Object.hasOwn(globalThis, key);
const previous = (globalThis as Record<string, unknown>)[key];
Object.defineProperty(globalThis, key, {
configurable: true,
writable: true,
value,
});
return () => {
if (hadValue) {
Object.defineProperty(globalThis, key, {
configurable: true,
writable: true,
value: previous,
});
} else {
delete (globalThis as Record<string, unknown>)[key];
}
};
}
/**
* Build a ReadableStream that yields `chunkCount` Uint8Array chunks at
* `intervalMs` apart. Each chunk is filled with a distinct byte value so
* we can verify ordering on the consumer side.
*/
function makeChunkedStream(
chunkSizes: number[],
intervalMs: number,
): {
stream: ReadableStream<Uint8Array>;
chunksEnqueued: { time: number; size: number }[];
} {
const chunksEnqueued: { time: number; size: number }[] = [];
const start = Date.now();
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
for (let i = 0; i < chunkSizes.length; i += 1) {
if (i > 0) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
const size = chunkSizes[i];
const chunk = new Uint8Array(size);
chunk.fill(i + 1);
chunksEnqueued.push({ time: Date.now() - start, size });
controller.enqueue(chunk);
}
controller.close();
},
});
return { stream, chunksEnqueued };
}
describe("plugin-elevenlabs TTS streaming", () => {
it("drains a multi-chunk stream and concatenates bytes in order", async () => {
const chunkSizes = [16, 32, 24, 8];
const { stream } = makeChunkedStream(chunkSizes, 5);
streamMock.mockResolvedValueOnce(stream);
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
expect(ttsHandler).toBeDefined();
const runtime = createTestRuntime();
// The plugin handler signature accepts (runtime, input).
const result = (await ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
"hello world",
)) as Uint8Array;
expect(result).toBeInstanceOf(Uint8Array);
const expectedTotal = chunkSizes.reduce((a, b) => a + b, 0);
expect(result.byteLength).toBe(expectedTotal);
// Verify chunk ordering: bytes from chunk 0 (filled with 1) come first,
// then chunk 1 (filled with 2), etc.
let cursor = 0;
for (let i = 0; i < chunkSizes.length; i += 1) {
const expected = i + 1;
for (let b = 0; b < chunkSizes[i]; b += 1) {
expect(result[cursor + b]).toBe(expected);
}
cursor += chunkSizes[i];
}
});
it("invokes the SDK with the configured voice + format params", async () => {
streamMock.mockReset();
const { stream } = makeChunkedStream([8], 0);
streamMock.mockResolvedValueOnce(stream);
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime({
ELEVENLABS_VOICE_ID: "voice-XYZ",
ELEVENLABS_OUTPUT_FORMAT: "pcm_16000",
});
await ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
{ text: "stream me" },
);
expect(streamMock).toHaveBeenCalledTimes(1);
const [voiceArg, optsArg] = streamMock.mock.calls[0] as [
string,
Record<string, unknown>,
];
expect(voiceArg).toBe("voice-XYZ");
expect(optsArg.text).toBe("stream me");
expect(optsArg.outputFormat).toBe("pcm_16000");
});
it("respects an explicit per-call format override", async () => {
streamMock.mockReset();
const { stream } = makeChunkedStream([4], 0);
streamMock.mockResolvedValueOnce(stream);
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime();
await ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
{ text: "override format", format: "pcm_16000" },
);
const [, optsArg] = streamMock.mock.calls[0] as [
string,
Record<string, unknown>,
];
expect(optsArg.outputFormat).toBe("pcm_16000");
});
it("propagates SDK errors instead of returning empty bytes", async () => {
streamMock.mockReset();
streamMock.mockRejectedValueOnce(new Error("upstream 503"));
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime();
await expect(
ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
"fail me",
),
).rejects.toThrow(/upstream 503/);
});
// Malformed provider response: the SDK resolves with no stream. The handler
// must throw rather than fabricate an empty audio buffer (issue #12797).
it.each([
null,
undefined,
])("throws on an empty TTS stream body (%s) instead of returning fake audio", async (emptyBody) => {
streamMock.mockReset();
streamMock.mockResolvedValueOnce(emptyBody);
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime();
await expect(
ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
"no stream",
),
).rejects.toThrow("Empty response body from ElevenLabs SDK");
});
it.each([
"",
" \n\t ",
null,
{ text: "" },
{ text: "hello", voiceId: " " },
{ text: "hello", model: "" },
{ text: "hello", format: "\t" },
])("rejects hostile TTS input before streaming %#", async (input) => {
streamMock.mockReset();
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime();
await expect(
ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
input as never,
),
).rejects.toThrow(/ElevenLabs TTS .*required|must be a non-empty string/);
expect(streamMock).not.toHaveBeenCalled();
});
it("uses a browser proxy without sending synthetic API credentials", async () => {
streamMock.mockReset();
clientConfigMock.mockReset();
const { stream } = makeChunkedStream([4], 0);
streamMock.mockResolvedValueOnce(stream);
const restoreDocument = setGlobalValue("document", {});
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime({
ELEVENLABS_API_KEY: undefined,
ELEVENLABS_BROWSER_URL: "https://elevenlabs-proxy.example/v1",
});
await ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
"browser proxy",
);
expect(clientConfigMock).toHaveBeenCalledWith({
baseUrl: "https://elevenlabs-proxy.example/v1",
});
expect(streamMock).toHaveBeenCalledTimes(1);
restoreDocument();
});
it("rejects browser TTS without an API key or browser proxy", async () => {
streamMock.mockReset();
clientConfigMock.mockReset();
const restoreDocument = setGlobalValue("document", {});
const { elevenLabsPlugin } = await import("../src/index.js");
const ttsHandler = elevenLabsPlugin.models?.TEXT_TO_SPEECH;
const runtime = createTestRuntime({
ELEVENLABS_API_KEY: undefined,
ELEVENLABS_BROWSER_URL: undefined,
});
await expect(
ttsHandler?.(
runtime as unknown as Parameters<NonNullable<typeof ttsHandler>>[0],
"missing proxy",
),
).rejects.toThrow("ELEVENLABS_API_KEY is required");
expect(clientConfigMock).not.toHaveBeenCalled();
expect(streamMock).not.toHaveBeenCalled();
restoreDocument();
});
});
describe("plugin-elevenlabs STT transcription", () => {
it("sends Buffer input with configured STT options", async () => {
convertMock.mockReset();
convertMock.mockResolvedValueOnce({ text: "transcribed buffer" });
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime({
ELEVENLABS_STT_MODEL_ID: "scribe_v1",
ELEVENLABS_STT_LANGUAGE_CODE: "en",
ELEVENLABS_STT_TIMESTAMPS_GRANULARITY: "word",
ELEVENLABS_STT_DIARIZE: "true",
ELEVENLABS_STT_NUM_SPEAKERS: "2",
ELEVENLABS_STT_TAG_AUDIO_EVENTS: "true",
});
const audio = Buffer.from([1, 2, 3, 4]);
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
audio,
),
).resolves.toBe("transcribed buffer");
expect(convertMock).toHaveBeenCalledWith({
modelId: "scribe_v1",
file: audio,
languageCode: "en",
timestampsGranularity: "word",
diarize: true,
numSpeakers: 2,
tagAudioEvents: true,
});
});
it("fetches audioUrl input and sends explicit none timestamp granularity", async () => {
convertMock.mockReset();
convertMock.mockResolvedValueOnce({
transcripts: [{ text: "left" }, { text: "right" }],
});
const fetchMock = vi.fn(async () => ({
ok: true,
arrayBuffer: async () => new Uint8Array([9, 8, 7]).buffer,
}));
const restoreFetch = setGlobalValue("fetch", fetchMock);
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime({
ELEVENLABS_STT_TIMESTAMPS_GRANULARITY: "none",
ELEVENLABS_STT_DIARIZE: "false",
ELEVENLABS_STT_TAG_AUDIO_EVENTS: "false",
});
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
{ audioUrl: "https://audio.example/file.wav" },
),
).resolves.toBe("left\nright");
expect(fetchMock).toHaveBeenCalledWith("https://audio.example/file.wav");
expect(convertMock).toHaveBeenCalledWith({
modelId: "scribe_v1",
file: Buffer.from([9, 8, 7]),
timestampsGranularity: "none",
});
restoreFetch();
});
it("throws when URL audio fetch fails before calling the SDK", async () => {
convertMock.mockReset();
const restoreFetch = setGlobalValue(
"fetch",
vi.fn(async () => ({
ok: false,
arrayBuffer: async () => new ArrayBuffer(0),
})),
);
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime();
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
"https://audio.example/missing.wav",
),
).rejects.toThrow(
"Failed to fetch audio from URL: https://audio.example/missing.wav",
);
expect(convertMock).not.toHaveBeenCalled();
restoreFetch();
});
it.each([
"",
"not a url",
"file:///etc/passwd",
"data:audio/wav;base64,AAAA",
{ audioUrl: "javascript:alert(1)" },
{ audioUrl: " " },
null,
{},
])("rejects hostile transcription URL input before fetch %#", async (input) => {
convertMock.mockReset();
const fetchMock = vi.fn();
const restoreFetch = setGlobalValue("fetch", fetchMock);
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime();
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
input as never,
),
).rejects.toThrow(/audioUrl|Invalid input type/);
expect(fetchMock).not.toHaveBeenCalled();
expect(convertMock).not.toHaveBeenCalled();
restoreFetch();
});
it("rejects unsupported STT enum values", async () => {
convertMock.mockReset();
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime({
ELEVENLABS_STT_MODEL_ID: "bad_model",
});
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
Buffer.from([1]),
),
).rejects.toThrow("Unsupported ElevenLabs STT model: bad_model");
expect(convertMock).not.toHaveBeenCalled();
});
it("rejects invalid numSpeakers before calling the SDK", async () => {
convertMock.mockReset();
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime({
ELEVENLABS_STT_DIARIZE: "true",
ELEVENLABS_STT_NUM_SPEAKERS: "abc",
});
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
Buffer.from([1]),
),
).rejects.toThrow(
"ELEVENLABS_STT_NUM_SPEAKERS must be an integer between 1 and 32",
);
expect(convertMock).not.toHaveBeenCalled();
});
// Provider rejection: the SDK throws. The handler must surface the failure to
// the caller rather than returning a fabricated/empty transcript (#12797).
it("propagates STT SDK errors instead of returning an empty transcript", async () => {
convertMock.mockReset();
convertMock.mockRejectedValueOnce(new Error("stt upstream 500"));
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime();
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
Buffer.from([1, 2, 3]),
),
).rejects.toThrow(/stt upstream 500/);
});
// Malformed provider response: the SDK resolves with no payload. The handler
// must throw rather than return an empty-string transcript (#12797).
it.each([
null,
undefined,
])("throws on an empty STT response (%s) instead of returning fake text", async (emptyResponse) => {
convertMock.mockReset();
convertMock.mockResolvedValueOnce(emptyResponse);
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime();
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
Buffer.from([1, 2, 3]),
),
).rejects.toThrow("Empty response from ElevenLabs STT API");
});
it("supports browser object URL input without a global Buffer", async () => {
convertMock.mockReset();
convertMock.mockResolvedValueOnce({ text: "browser transcript" });
const blob = new Blob([new Uint8Array([5, 4, 3])], {
type: "audio/wav",
});
const restoreDocument = setGlobalValue("document", {});
const restoreBuffer = setGlobalValue("Buffer", undefined);
const restoreFetch = setGlobalValue(
"fetch",
vi.fn(async () => ({
ok: true,
blob: async () => blob,
arrayBuffer: async () => new Uint8Array([5, 4, 3]).buffer,
})),
);
const { elevenLabsPlugin } = await import("../src/index.js");
const transcriptionHandler = elevenLabsPlugin.models?.TRANSCRIPTION;
const runtime = createTestRuntime();
await expect(
transcriptionHandler?.(
runtime as unknown as Parameters<
NonNullable<typeof transcriptionHandler>
>[0],
{ audioUrl: "https://audio.example/browser.wav" },
),
).resolves.toBe("browser transcript");
expect(convertMock).toHaveBeenCalledWith({
modelId: "scribe_v1",
file: blob,
timestampsGranularity: "word",
});
restoreFetch();
restoreBuffer();
restoreDocument();
});
});