mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-05 02:19:44 +00:00
Phase 1 of the TTS params/layout/agent-override plan.
Capability schema:
- `ParamSchema.Group` field (`"basic"` default, `"advanced"` when set).
- Tag existing advanced params across openai/elevenlabs/minimax/gemini.
Gemini:
- Expose `temperature` (basic, 0.0–2.0, default 1.0, subtle-effect note),
`seed`, `presencePenalty`, `frequencyPenalty` (advanced, experimental).
- Merge into `generationConfig` via explicit-presence resolvers so
nil-params bodies stay byte-equivalent.
ElevenLabs:
- `output_format` enum (27 variants, default `mp3_44100_128`, advanced).
- `FormatMeta` lookup drives SynthResult MIME + extension.
- URL built via `net/url.Values.Encode()`; regex pre-validation for
`output_format` (`^[a-z0-9_]+$`) and `language_code`
(`^[a-z]{2,3}(-[A-Z]{2})?$`) blocks query-string injection.
- Telegram opus contract preserved: `opts.Format=="opus"` forces
`audio/ogg; codecs=opus` regardless of user-set `output_format`.
MiniMax:
- `language_boost` (basic enum), `subtitle_enable` (basic bool),
`pronunciation_dict` (advanced text, 8KB cap, wrapped as
`{"tone":[...]}`). Parse failure logs length only + omits.
Validation:
- `audio.ValidateParams` enforces Min/Max/Enum + rejects unknown keys.
- Wired into `/v1/tts/synthesize` and `/v1/tts/config` write paths.
- `loadParamsBlob` capped at 16KB.
- i18n keys `MsgTtsParamOutOfRange`, `MsgTtsParamInvalidJSON`,
`MsgTtsParamUnknownKey` added to en/vi/zh catalogs.
Tests:
- Golden `testdata/default_body.golden.json` per provider (gemini,
elevenlabs, minimax, openai) checked in; invariant tests diff
against file instead of self-referential capture.
- Round-trip tests for each new param + Telegram opus contract +
URL-injection attempts.
141 lines
4.6 KiB
Go
141 lines
4.6 KiB
Go
package gemini_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio"
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio/gemini"
|
|
)
|
|
|
|
// TestSynthesize_Params_Temperature verifies temperature lands in generationConfig.
|
|
func TestSynthesize_Params_Temperature(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{
|
|
Params: map[string]any{"temperature": 1.5},
|
|
})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
if v, _ := gc["temperature"].(float64); v != 1.5 {
|
|
t.Errorf("temperature: got %v, want 1.5", gc["temperature"])
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_Params_Seed verifies seed lands in generationConfig.
|
|
func TestSynthesize_Params_Seed(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{
|
|
Params: map[string]any{"seed": 12345},
|
|
})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
// JSON round-trip: int comes back as float64.
|
|
if v, _ := gc["seed"].(float64); int(v) != 12345 {
|
|
t.Errorf("seed: got %v, want 12345", gc["seed"])
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_Params_PresencePenalty verifies presencePenalty in generationConfig.
|
|
func TestSynthesize_Params_PresencePenalty(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{
|
|
Params: map[string]any{"presencePenalty": 0.3},
|
|
})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
if v, _ := gc["presencePenalty"].(float64); v != 0.3 {
|
|
t.Errorf("presencePenalty: got %v, want 0.3", gc["presencePenalty"])
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_Params_FrequencyPenalty verifies frequencyPenalty in generationConfig.
|
|
func TestSynthesize_Params_FrequencyPenalty(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{
|
|
Params: map[string]any{"frequencyPenalty": -0.1},
|
|
})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
if v, _ := gc["frequencyPenalty"].(float64); v != -0.1 {
|
|
t.Errorf("frequencyPenalty: got %v, want -0.1", gc["frequencyPenalty"])
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_Params_AllFour verifies all four params land together.
|
|
func TestSynthesize_Params_AllFour(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{
|
|
Params: map[string]any{
|
|
"temperature": 1.5,
|
|
"seed": 12345,
|
|
"presencePenalty": 0.3,
|
|
"frequencyPenalty": -0.1,
|
|
},
|
|
})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
if v, _ := gc["temperature"].(float64); v != 1.5 {
|
|
t.Errorf("temperature: got %v, want 1.5", gc["temperature"])
|
|
}
|
|
if v, _ := gc["seed"].(float64); int(v) != 12345 {
|
|
t.Errorf("seed: got %v, want 12345", gc["seed"])
|
|
}
|
|
if v, _ := gc["presencePenalty"].(float64); v != 0.3 {
|
|
t.Errorf("presencePenalty: got %v, want 0.3", gc["presencePenalty"])
|
|
}
|
|
if v, _ := gc["frequencyPenalty"].(float64); v != -0.1 {
|
|
t.Errorf("frequencyPenalty: got %v, want -0.1", gc["frequencyPenalty"])
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_Params_NilParams_NoExtraKeys verifies nil params produces no
|
|
// extra keys in generationConfig beyond the mandatory ones.
|
|
func TestSynthesize_Params_NilParams_NoExtraKeys(t *testing.T) {
|
|
cfg := gemini.Config{APIKey: "k"}
|
|
body, _ := captureGeminiBody(t, cfg, audio.TTSOptions{})
|
|
gc := assertGeminiGenerationConfig(t, body)
|
|
|
|
for _, extra := range []string{"temperature", "seed", "presencePenalty", "frequencyPenalty"} {
|
|
if _, ok := gc[extra]; ok {
|
|
t.Errorf("nil params: generationConfig must not contain %q", extra)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCapabilities_Gemini_HasFourParams confirms Capabilities returns exactly
|
|
// the four documented params with correct Group tags.
|
|
func TestCapabilities_Gemini_HasFourParams(t *testing.T) {
|
|
p := gemini.NewProvider(gemini.Config{APIKey: "k"})
|
|
caps := p.Capabilities()
|
|
|
|
type want struct {
|
|
group string
|
|
}
|
|
expected := map[string]want{
|
|
"temperature": {group: ""},
|
|
"seed": {group: "advanced"},
|
|
"presencePenalty": {group: "advanced"},
|
|
"frequencyPenalty": {group: "advanced"},
|
|
}
|
|
|
|
found := map[string]bool{}
|
|
for _, p := range caps.Params {
|
|
if w, ok := expected[p.Key]; ok {
|
|
found[p.Key] = true
|
|
if p.Group != w.group {
|
|
t.Errorf("param %q: Group got %q, want %q", p.Key, p.Group, w.group)
|
|
}
|
|
}
|
|
}
|
|
for key := range expected {
|
|
if !found[key] {
|
|
t.Errorf("param %q not found in Capabilities", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// assertGeminiGenerationConfig extracts generationConfig from the body and fails
|
|
// if it is missing or not a map.
|
|
func assertGeminiGenerationConfig(t *testing.T, body map[string]any) map[string]any {
|
|
t.Helper()
|
|
gc, ok := body["generationConfig"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("generationConfig missing or not a map: %#v", body["generationConfig"])
|
|
}
|
|
return gc
|
|
}
|