Files
viettranx 613b6e38d7 feat(tts): provider capabilities schema + Gemini TTS + dynamic param forms
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.
2026-04-20 00:05:19 +07:00

69 lines
2.1 KiB
Go

package gemini
import (
"bytes"
"encoding/binary"
"testing"
)
// TestWrap_RIFFHeader_Bytes verifies every field of the 44-byte RIFF/WAV header
// produced by Wrap() for a 1024-byte PCM payload.
func TestWrap_RIFFHeader_Bytes(t *testing.T) {
pcm := make([]byte, 1024)
wav := Wrap(pcm)
if !bytes.Equal(wav[0:4], []byte("RIFF")) {
t.Errorf("bytes[0:4] = %q, want RIFF", wav[0:4])
}
if got := binary.LittleEndian.Uint32(wav[4:8]); got != 36+1024 {
t.Errorf("chunk size = %d, want %d", got, 36+1024)
}
if !bytes.Equal(wav[8:12], []byte("WAVE")) {
t.Errorf("bytes[8:12] = %q, want WAVE", wav[8:12])
}
if !bytes.Equal(wav[12:16], []byte("fmt ")) {
t.Errorf("bytes[12:16] = %q, want 'fmt '", wav[12:16])
}
if got := binary.LittleEndian.Uint32(wav[16:20]); got != 16 {
t.Errorf("fmt chunk size = %d, want 16", got)
}
if got := binary.LittleEndian.Uint16(wav[20:22]); got != 1 {
t.Errorf("audio format = %d, want 1 (PCM)", got)
}
if got := binary.LittleEndian.Uint16(wav[22:24]); got != 1 {
t.Errorf("channels = %d, want 1 (mono)", got)
}
if got := binary.LittleEndian.Uint32(wav[24:28]); got != 24000 {
t.Errorf("sample rate = %d, want 24000", got)
}
if got := binary.LittleEndian.Uint32(wav[28:32]); got != 48000 {
t.Errorf("byte rate = %d, want 48000", got)
}
if got := binary.LittleEndian.Uint16(wav[32:34]); got != 2 {
t.Errorf("block align = %d, want 2", got)
}
if got := binary.LittleEndian.Uint16(wav[34:36]); got != 16 {
t.Errorf("bits per sample = %d, want 16", got)
}
if !bytes.Equal(wav[36:40], []byte("data")) {
t.Errorf("bytes[36:40] = %q, want 'data'", wav[36:40])
}
if got := binary.LittleEndian.Uint32(wav[40:44]); got != 1024 {
t.Errorf("data chunk size = %d, want 1024", got)
}
if !bytes.Equal(wav[44:], pcm) {
t.Error("PCM payload mismatch after WAV header")
}
}
// TestWrap_EmptyPCM verifies a 44-byte header + zero data payload.
func TestWrap_EmptyPCM(t *testing.T) {
wav := Wrap([]byte{})
if len(wav) != wavHeaderSize {
t.Errorf("len(wav) = %d, want %d", len(wav), wavHeaderSize)
}
if got := binary.LittleEndian.Uint32(wav[40:44]); got != 0 {
t.Errorf("data chunk size = %d, want 0", got)
}
}