mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 18:16:09 +00:00
Baseline groundwork for TTS expansion plan. Introduces a capabilities system that the follow-up plan (phase-01..04) builds on. Backend: - `audio.ParamSchema` / `ProviderCapabilities` types + per-provider capabilities.go for edge, elevenlabs, minimax, openai, gemini. - Gemini TTS provider (client, models, voices, wav encoder, multi-speaker via SpeakerVoice, audio-tag aware prompts). - TTSOptions gains `Params map[string]any` (read-only) + `Speakers`. - `VoiceListProvider` interface decouples HTTP voice handler from provider-specific impls. - `nested_keys.go` resolves dot-separated param paths for nested provider bodies (voice_settings.stability etc.). - Characterization + defaults-invariant tests per provider lock nil-params byte-equivalence before new params land. - `/v1/tts/capabilities` HTTP endpoint + integration coverage. - Dual-read tests (PG + SQLite) for tts_config. Frontend (web + desktop): - `DynamicParamForm` with depends-on evaluation, split into fields/logic modules. Slider primitive added. - `AudioTagPicker` + `MultiSpeakerEditor` for Gemini. - `voice-picker` refactored toward portal UX; combobox tightened. - tts-capabilities API client + typed hooks. - i18n catalogs (en/vi/zh) expanded; parity tests guard key drift. - TTS page reorganised (voice-model-section removed; playground + credentials + provider-setup split cleanly). Docs: codebase-summary, project-changelog, tts-provider-capabilities.
87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
package openai_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio"
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio/openai"
|
|
)
|
|
|
|
// captureOpenAIBody sends a Synthesize request to a mock server and
|
|
// returns the raw JSON body bytes that the provider sent.
|
|
func captureOpenAIBody(t *testing.T, cfg openai.Config, opts audio.TTSOptions) []byte {
|
|
t.Helper()
|
|
var captured []byte
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
b, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("read body: %v", err)
|
|
}
|
|
captured = b
|
|
w.Header().Set("Content-Type", "audio/mpeg")
|
|
w.Write([]byte("AUDIO"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
cfg.APIBase = srv.URL
|
|
p := openai.NewProvider(cfg)
|
|
_, err := p.Synthesize(t.Context(), "hello", opts)
|
|
if err != nil {
|
|
t.Fatalf("Synthesize failed: %v", err)
|
|
}
|
|
return captured
|
|
}
|
|
|
|
// TestCharacterization_DefaultOpts captures the request body for default opts
|
|
// and asserts the expected JSON fields. This is a golden-fixture characterization
|
|
// test — the exact shape MUST remain byte-compatible after the Synthesize refactor.
|
|
func TestCharacterization_DefaultOpts(t *testing.T) {
|
|
cfg := openai.Config{APIKey: "test-key"}
|
|
body := captureOpenAIBody(t, cfg, audio.TTSOptions{})
|
|
|
|
var m map[string]any
|
|
if err := json.Unmarshal(body, &m); err != nil {
|
|
t.Fatalf("body not valid JSON: %v — body: %s", err, body)
|
|
}
|
|
|
|
// Golden assertions for default-opts characterization.
|
|
assertJSONField(t, m, "model", "gpt-4o-mini-tts")
|
|
assertJSONField(t, m, "voice", "alloy")
|
|
assertJSONField(t, m, "response_format", "mp3")
|
|
assertJSONField(t, m, "input", "hello")
|
|
}
|
|
|
|
// TestCharacterization_ExplicitVoiceModel verifies the body reflects
|
|
// overridden voice/model without mutating any default path.
|
|
func TestCharacterization_ExplicitVoiceModel(t *testing.T) {
|
|
cfg := openai.Config{APIKey: "test-key"}
|
|
body := captureOpenAIBody(t, cfg, audio.TTSOptions{Voice: "nova", Model: "tts-1"})
|
|
|
|
var m map[string]any
|
|
if err := json.Unmarshal(body, &m); err != nil {
|
|
t.Fatalf("body not valid JSON: %v", err)
|
|
}
|
|
|
|
assertJSONField(t, m, "voice", "nova")
|
|
assertJSONField(t, m, "model", "tts-1")
|
|
}
|
|
|
|
func assertJSONField(t *testing.T, m map[string]any, key, want string) {
|
|
t.Helper()
|
|
v, ok := m[key]
|
|
if !ok {
|
|
t.Errorf("missing JSON field %q", key)
|
|
return
|
|
}
|
|
if s, _ := v.(string); s != want {
|
|
t.Errorf("field %q: got %q, want %q", key, s, want)
|
|
}
|
|
}
|