From 5a7ada63bfe9891168ea354d7a7c8b6c2db84ca4 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Thu, 8 Oct 2026 20:20:37 +0000 Subject: [PATCH] fix(cli): validate the complete TTS speed argument (#5264) --- packages/cli/src/commands/tts.test.ts | 36 +++++++++++++++++++++++++++ packages/cli/src/commands/tts.ts | 4 +-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/tts.test.ts b/packages/cli/src/commands/tts.test.ts index 914b399edb..287ee2d788 100644 --- a/packages/cli/src/commands/tts.test.ts +++ b/packages/cli/src/commands/tts.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { runCommand } from "citty"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -44,6 +45,41 @@ describe("tts command", () => { ); }); + it.each(["0.05", "1x", "1.5junk", "1 2", "", " ", "NaN", "Infinity", "0", "-1", "3.01"])( + "rejects invalid speed %j before synthesis", + async (speed) => { + const errors = vi.spyOn(console, "error").mockImplementation(() => {}); + await expect( + runCommand(ttsCommand, { rawArgs: ["Hello", "--speed", speed, "--json"] }), + ).rejects.toThrow(); + expect(synthesizeMock).not.toHaveBeenCalled(); + expect(errors.mock.calls.flat().join(" ")).toContain("between 0.1 and 3.0"); + }, + ); + + it.each([ + ["0.1", 0.1], + ["3", 3], + ["1e-1", 0.1], + [" 1.25 ", 1.25], + ])("uses the complete valid speed %j", async (speed, expected) => { + await runCommand(ttsCommand, { rawArgs: ["Hello", "--speed", speed, "--json"] }); + expect(synthesizeMock).toHaveBeenCalledWith( + "Hello", + expect.any(String), + expect.objectContaining({ speed: expected }), + ); + }); + + it("defaults the speed only when the argument is omitted", async () => { + await runCommand(ttsCommand, { rawArgs: ["Hello", "--json"] }); + expect(synthesizeMock).toHaveBeenCalledWith( + "Hello", + expect.any(String), + expect.objectContaining({ speed: 1 }), + ); + }); + it("rejects text words with --text-file even under --list", async () => { const input = join(dir, "script.txt"); writeFileSync(input, "From the file\n"); diff --git a/packages/cli/src/commands/tts.ts b/packages/cli/src/commands/tts.ts index 2e15a0b1f2..91ce6f48fe 100644 --- a/packages/cli/src/commands/tts.ts +++ b/packages/cli/src/commands/tts.ts @@ -121,9 +121,9 @@ export default defineCommand({ // ── Resolve output path ─────────────────────────────────────────── const output = resolve(args.output ?? "speech.wav"); const voice = args.voice ?? DEFAULT_VOICE; - const speed = args.speed ? parseFloat(args.speed) : 1.0; + const speed = args.speed === undefined ? 1.0 : Number(args.speed); - if (isNaN(speed) || speed <= 0 || speed > 3) { + if (!Number.isFinite(speed) || speed < 0.1 || speed > 3) { console.error(c.error("Speed must be a number between 0.1 and 3.0")); failCommand(); }