Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions cmd/speak.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
55 changes: 55 additions & 0 deletions cmd/speak_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading