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

63 lines
1.7 KiB
Go

// Package gemini implements Google Gemini TTS via generateContent API.
// Gemini returns base64 PCM 16-bit signed little-endian 24kHz mono; this
// package wraps it in a 44-byte RIFF/WAV header so browsers can play it.
package gemini
import "encoding/binary"
// WAV header constants for 24kHz mono 16-bit PCM.
const (
sampleRate24k uint32 = 24000
channelsMono uint16 = 1
bitsPerSample uint16 = 16
audioFormatPCM uint16 = 1
fmtChunkSize uint32 = 16
wavHeaderSize = 44
)
// Wrap prepends a RIFF/WAV header to raw PCM bytes and returns playable WAV.
// PCM must be 16-bit signed little-endian at 24kHz mono.
func Wrap(pcm []byte) []byte {
dataSize := uint32(len(pcm))
byteRate := uint32(sampleRate24k) * uint32(channelsMono) * uint32(bitsPerSample) / 8
blockAlign := channelsMono * bitsPerSample / 8
out := make([]byte, wavHeaderSize+dataSize)
pos := 0
// RIFF chunk descriptor
copy(out[pos:], "RIFF")
pos += 4
binary.LittleEndian.PutUint32(out[pos:], 36+dataSize) // chunk size = 36 + data
pos += 4
copy(out[pos:], "WAVE")
pos += 4
// fmt sub-chunk
copy(out[pos:], "fmt ")
pos += 4
binary.LittleEndian.PutUint32(out[pos:], fmtChunkSize)
pos += 4
binary.LittleEndian.PutUint16(out[pos:], audioFormatPCM)
pos += 2
binary.LittleEndian.PutUint16(out[pos:], channelsMono)
pos += 2
binary.LittleEndian.PutUint32(out[pos:], sampleRate24k)
pos += 4
binary.LittleEndian.PutUint32(out[pos:], byteRate)
pos += 4
binary.LittleEndian.PutUint16(out[pos:], blockAlign)
pos += 2
binary.LittleEndian.PutUint16(out[pos:], bitsPerSample)
pos += 2
// data sub-chunk
copy(out[pos:], "data")
pos += 4
binary.LittleEndian.PutUint32(out[pos:], dataSize)
pos += 4
copy(out[pos:], pcm)
return out
}