diff --git a/CHANGELOG.md b/CHANGELOG.md index 91061d4..f5d439e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- Explicit `--format` values now override output-extension inference, preserving the requested ElevenLabs audio format. (#21/#22, thanks @100yenadmin) + ## 0.4.0 - 2026-06-11 ### Added - Added live-tested 60db provider support via `/default-voices`, `/myvoices`, and `/tts-synthesize`, including bounded NDJSON decoding, incomplete-response rejection, and PCM-to-WAV playback. (#20, thanks @manishEMS47) diff --git a/cmd/speak.go b/cmd/speak.go index 82b6d38..d21ed92 100644 --- a/cmd/speak.go +++ b/cmd/speak.go @@ -108,10 +108,13 @@ func init() { return err } - // If user provided output path with a known extension, infer a compatible format. + // If user provided output path with a known extension, infer a compatible format + // unless --format was explicitly set. Explicit format selection should always win. if opts.outputPath != "" { - if inferred := inferFormatFromExt(opts.outputPath); inferred != "" { - opts.outputFmt = inferred + if !cmd.Flags().Changed("format") { + if inferred := inferFormatFromExt(opts.outputPath); inferred != "" { + opts.outputFmt = inferred + } } // Disable playback when -o is set, unless --play was explicitly provided if !cmd.Flags().Changed("play") { diff --git a/cmd/speak_integration_test.go b/cmd/speak_integration_test.go index fe8067b..c747d5b 100644 --- a/cmd/speak_integration_test.go +++ b/cmd/speak_integration_test.go @@ -114,6 +114,61 @@ func TestSpeakCommand_FlagsBuildRequestAndMetrics(t *testing.T) { } } +func TestSpeakCommand_ExplicitFormatOverridesOutputExtension(t *testing.T) { + t.Helper() + resetProviderEnv(t) + resetRootCommandState() + + const voiceID = "abc1234567890123" + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.URL.Path, "/v1/text-to-speech/") { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if path.Base(r.URL.Path) != voiceID { + t.Fatalf("expected voice ID %q, got %q", voiceID, path.Base(r.URL.Path)) + } + if got := r.URL.Query().Get("output_format"); got != "mp3_44100_192" { + t.Fatalf("expected explicit output_format query mp3_44100_192, got %q", got) + } + + var got map[string]any + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatalf("decode body: %v", err) + } + if _, ok := got["output_format"]; ok { + t.Fatalf("expected output_format to be omitted from body, got %v", got["output_format"]) + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("audio-bytes")) + })) + defer srv.Close() + + tmp := t.TempDir() + outPath := tmp + "/out.mp3" + + rootCmd.SetArgs([]string{ + "--api-key", "testkey", + "--base-url", srv.URL, + "speak", + "--voice-id", voiceID, + "--stream=false", + "--play=false", + "--output", outPath, + "--format", "mp3_44100_192", + "Hello world", + }) + + if err := rootCmd.Execute(); err != nil { + t.Fatalf("speak command failed: %v", err) + } + + if _, err := os.Stat(outPath); err != nil { + t.Fatalf("expected output file to be written: %v", err) + } +} + func TestSpeakCommand_SixtyDBMetricsOmitElevenLabsModel(t *testing.T) { t.Helper() resetProviderEnv(t)