From 946c24d67439c00bbf31073ba3d273bfe0d676e3 Mon Sep 17 00:00:00 2001 From: Hiago Silva <97215740+Huntterxx@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:22:09 -0300 Subject: [PATCH] fix: validate edge tts output file is non-empty before reporting success (#43385) thanks @Huntterxx Merged after review.\n\nSmall, scoped fix: treat 0-byte Edge TTS output as failure so provider fallback can continue. --- src/tts/edge-tts-validation.test.ts | 69 +++++++++++++++++++++++++++++ src/tts/tts-core.ts | 8 +++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/tts/edge-tts-validation.test.ts diff --git a/src/tts/edge-tts-validation.test.ts b/src/tts/edge-tts-validation.test.ts new file mode 100644 index 00000000000..08697a2c9bd --- /dev/null +++ b/src/tts/edge-tts-validation.test.ts @@ -0,0 +1,69 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +let mockTtsPromise = vi.fn<(text: string, filePath: string) => Promise>(); + +vi.mock("node-edge-tts", () => ({ + EdgeTTS: class { + ttsPromise(text: string, filePath: string) { + return mockTtsPromise(text, filePath); + } + }, +})); + +const { edgeTTS } = await import("./tts-core.js"); + +const baseEdgeConfig = { + enabled: true, + voice: "en-US-MichelleNeural", + lang: "en-US", + outputFormat: "audio-24khz-48kbitrate-mono-mp3", + outputFormatConfigured: false, + saveSubtitles: false, +}; + +describe("edgeTTS – empty audio validation", () => { + let tempDir: string; + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("throws when the output file is 0 bytes", async () => { + tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-")); + const outputPath = path.join(tempDir, "voice.mp3"); + + mockTtsPromise = vi.fn(async (_text: string, filePath: string) => { + writeFileSync(filePath, ""); + }); + + await expect( + edgeTTS({ + text: "Hello", + outputPath, + config: baseEdgeConfig, + timeoutMs: 10000, + }), + ).rejects.toThrow("Edge TTS produced empty audio file"); + }); + + it("succeeds when the output file has content", async () => { + tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-")); + const outputPath = path.join(tempDir, "voice.mp3"); + + mockTtsPromise = vi.fn(async (_text: string, filePath: string) => { + writeFileSync(filePath, Buffer.from([0xff, 0xfb, 0x90, 0x00])); + }); + + await expect( + edgeTTS({ + text: "Hello", + outputPath, + config: baseEdgeConfig, + timeoutMs: 10000, + }), + ).resolves.toBeUndefined(); + }); +}); diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 279fc3cc1ed..93325c8fb06 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -1,4 +1,4 @@ -import { rmSync } from "node:fs"; +import { rmSync, statSync } from "node:fs"; import { completeSimple, type TextContent } from "@mariozechner/pi-ai"; import { EdgeTTS } from "node-edge-tts"; import { ensureCustomApiRegistered } from "../agents/custom-api-registry.js"; @@ -715,4 +715,10 @@ export async function edgeTTS(params: { timeout: config.timeoutMs ?? timeoutMs, }); await tts.ttsPromise(text, outputPath); + + const { size } = statSync(outputPath); + + if (size === 0) { + throw new Error("Edge TTS produced empty audio file"); + } }