mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-01 08:20:39 +00:00
Post-review cleanup of Phase 4. Closes Finding #9 properly and corrects the Finding #13 documentation lie surfaced in the code-review report. Capability schema: - Replace `AgentOverridable bool` with `AgentOverridableAs string` on ParamSchema. Empty string = not overridable; non-empty = the generic key alias (`"speed"`, `"emotion"`, `"style"`). - Each provider declaration now carries the alias inline, so the generic↔native mapping has a single TS-readable source. Frontend: - Web `tts-override-block.tsx` drops the inline `GENERIC_TO_NATIVE` literal and derives the bidirectional adapter from the filtered capability params (each param self-describes its alias). Adapter tests rewritten around the new shape. - Desktop `AgentDetailPanel.tsx` drops the 45-line inline IIFE in favour of a new `<TtsOverrideFineTune>` component that uses the same alias-based mapping. Backend: - Move `AgentTTSParamsAllowedKeys` + `ValidateAgentTTSParams` to `internal/audio/agent_params_adapter.go`. HTTP `validate.go` and WS `gateway/methods/agents_update.go` both delegate, eliminating the duplicated `{speed, emotion, style}` literal. Cleanup: - Delete orphan i18n keys `MsgTtsParamInvalidJSON` and `MsgTtsParamDependsOn` from `keys.go` + en/vi/zh catalogs (no in-code references; DependsOn is FE-only, JSON parse failures already surface via slog). Documentation: - `prompt-settings-section.tsx` Finding #13 comment rewritten to honestly describe the best-effort merge into a fresh local copy of the cached `otherConfig` prop. Concurrent-tab clobber remains possible — server-side JSON-merge-patch endpoint planned for v2. Tests: 9 backend suites (race), web 217/217, desktop build clean, both Go build tags pass.
135 lines
4.2 KiB
Go
135 lines
4.2 KiB
Go
package audio
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
)
|
|
|
|
// AgentTTSParamsAllowedKeys is the allow-list of generic keys that agents may
|
|
// store in other_config.tts_params. Any key outside this set is rejected at
|
|
// write time and silently dropped here for defense-in-depth.
|
|
// Both internal/http and internal/gateway/methods import this to avoid
|
|
// duplicating the literal (Action D: DRY).
|
|
var AgentTTSParamsAllowedKeys = map[string]bool{
|
|
"speed": true,
|
|
"emotion": true,
|
|
"style": true,
|
|
}
|
|
|
|
// agentOverrideKeys aliases the exported map for internal use in AdaptAgentParams.
|
|
var agentOverrideKeys = AgentTTSParamsAllowedKeys
|
|
|
|
// ValidateAgentTTSParams returns an error if ttsParams contains any key not in
|
|
// the allow-list. Values are not type-checked here — providers handle coercion
|
|
// at synthesis time.
|
|
func ValidateAgentTTSParams(ttsParams map[string]any) error {
|
|
for k := range ttsParams {
|
|
if !AgentTTSParamsAllowedKeys[k] {
|
|
return fmt.Errorf("tts_params key %q is not allowed; valid keys: speed, emotion, style", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AdaptAgentParams maps generic agent override keys (stored in
|
|
// agents.other_config.tts_params) to the provider-specific param keys that
|
|
// each provider's Synthesize implementation expects in opts.Params.
|
|
//
|
|
// Only the three allow-listed generic keys (speed, emotion, style) are
|
|
// translated. Unknown generic keys are silently dropped — defense-in-depth
|
|
// against DB state from before the allow-list was enforced.
|
|
//
|
|
// Absent generic keys are never written to the output map, so callers that
|
|
// merge the result into opts.Params do not accidentally zero-out provider
|
|
// defaults.
|
|
//
|
|
// CRITICAL (Finding #1): This must be called PER-ATTEMPT inside the fallback
|
|
// loop, not once before. Each attempt may use a different provider, and the
|
|
// mapping is provider-specific.
|
|
//
|
|
// Adapter table:
|
|
//
|
|
// generic key | openai | elevenlabs | edge | minimax | gemini
|
|
// ------------|-------------|------------------------|------|---------|-------
|
|
// speed | speed | voice_settings.speed | skip | speed | skip
|
|
// emotion | skip | skip | skip | emotion | skip
|
|
// style | skip | voice_settings.style | skip | skip | skip
|
|
func AdaptAgentParams(generic map[string]any, provider string) map[string]any {
|
|
if len(generic) == 0 {
|
|
return nil
|
|
}
|
|
|
|
out := make(map[string]any, len(generic))
|
|
|
|
switch provider {
|
|
case "openai":
|
|
if v, ok := generic["speed"]; ok {
|
|
out["speed"] = v
|
|
}
|
|
case "elevenlabs":
|
|
if v, ok := generic["speed"]; ok {
|
|
out["voice_settings.speed"] = v
|
|
}
|
|
if v, ok := generic["style"]; ok {
|
|
out["voice_settings.style"] = v
|
|
}
|
|
case "edge":
|
|
// No compatible generic keys — edge uses "rate" in a different semantic range.
|
|
case "minimax":
|
|
if v, ok := generic["speed"]; ok {
|
|
out["speed"] = v
|
|
}
|
|
if v, ok := generic["emotion"]; ok {
|
|
out["emotion"] = v
|
|
}
|
|
case "gemini":
|
|
// No compatible generic keys — Gemini uses audio tags in prompt text.
|
|
default:
|
|
// Unknown provider — return empty map. Log at Info for observability.
|
|
slog.Info("tts.agent.params.dropped", "provider", provider, "reason", "unknown provider")
|
|
return nil
|
|
}
|
|
|
|
// Log dropped keys (generic keys present but not mapped for this provider).
|
|
if len(generic) > 0 && len(out) == 0 {
|
|
slog.Info("tts.agent.params.dropped", "provider", provider, "dropped_keys", genericKeys(generic))
|
|
} else if len(generic) > len(out) {
|
|
// Some keys were mapped, some were dropped.
|
|
var dropped []string
|
|
for k := range generic {
|
|
if agentOverrideKeys[k] {
|
|
// Check if it landed in out (by checking the possible output key for this provider)
|
|
found := false
|
|
switch provider {
|
|
case "openai":
|
|
found = k == "speed"
|
|
case "elevenlabs":
|
|
found = k == "speed" || k == "style"
|
|
case "minimax":
|
|
found = k == "speed" || k == "emotion"
|
|
}
|
|
if !found {
|
|
dropped = append(dropped, k)
|
|
}
|
|
}
|
|
}
|
|
if len(dropped) > 0 {
|
|
slog.Info("tts.agent.params.dropped", "provider", provider, "dropped_keys", dropped)
|
|
}
|
|
}
|
|
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// genericKeys returns the keys of the map as a slice (for logging).
|
|
func genericKeys(m map[string]any) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|