Replace the browser's robotic Web Speech API (espeak on Chromium) as the
primary read-aloud path with server-side Piper neural TTS, served by petal
and kept fully offline on millenia next to Ollama.
- internal/tts: proxy short passages to Piper, transcode WAV -> mp3/opus via
ffmpeg, content-addressed disk cache (instant re-taps). Each Piper instance
loads one voice, so language routes to its own endpoint:{voice}. Unknown
language -> 404 so the client falls back to Web Speech. UTF-8-safe truncation
for multibyte (Chinese) text.
- config: TTS_ENDPOINT / TTS_ENDPOINT_ZH / TTS_VOICE_EN / TTS_VOICE_ZH /
TTS_CACHE_DIR / TTS_TIMEOUT / TTS_AUDIO_FORMAT. Route mounts only when
TTS_ENDPOINT is set; otherwise unchanged behavior.
- web/audio/speech.ts: speak() hits /api/tts first, falls back to Web Speech on
any failure; rapid-tap-safe via a request token. Call sites unchanged.
- deploy/: Piper user systemd units (EN :5005, ZH :5006), setup script, README.
English (en_US-amy-medium) and Chinese (zh_CN-huayan-medium) are both live.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Config holds all runtime configuration, loaded from environment variables.
|
|
// See .env.example for the full list and defaults.
|
|
type Config struct {
|
|
Port string
|
|
BaseURL string
|
|
DatabasePath string
|
|
ImageDir string // on-disk store for editor image uploads
|
|
|
|
// LLM
|
|
LLMBackend string // "vllm" | "ollama"
|
|
LLMEndpoint string
|
|
LLMModel string // checkpoint model (small, fast)
|
|
LLMChatModel string // Ask Petal model; falls back to LLMModel if empty
|
|
LLMTimeout time.Duration
|
|
|
|
// TTS (read-aloud). Off unless TTSEndpoint is set — when empty, the /api/tts
|
|
// route isn't mounted and the frontend falls back to the browser's Web Speech
|
|
// API. Endpoint points at a local Piper HTTP server.
|
|
TTSEndpoint string // Piper instance serving the English voice
|
|
TTSEndpointZH string // Piper instance serving the Chinese voice; empty = zh falls back to Web Speech
|
|
TTSVoiceEN string // Piper voice id for English (e.g. en_US-amy-medium)
|
|
TTSVoiceZH string // Piper voice id for Chinese (e.g. zh_CN-huayan-medium)
|
|
TTSCacheDir string // on-disk store for synthesized clips (content-addressed)
|
|
TTSTimeout time.Duration
|
|
TTSFormat string // mp3 | opus | wav — mp3/opus transcode Piper's WAV via ffmpeg
|
|
|
|
// Auth (deferred — not wired in the local-dev build, kept for later)
|
|
AuthentikURL string
|
|
AuthentikClientID string
|
|
AuthentikClientSecret string
|
|
SessionSecret string
|
|
}
|
|
|
|
// Load reads configuration from the environment, applying sane local-dev defaults.
|
|
func Load() *Config {
|
|
return &Config{
|
|
Port: env("PORT", "8080"),
|
|
BaseURL: env("BASE_URL", "http://localhost:8080"),
|
|
DatabasePath: env("DATABASE_PATH", "./data/petal.db"),
|
|
ImageDir: env("IMAGE_DIR", "./data/images"),
|
|
|
|
LLMBackend: env("LLM_BACKEND", "vllm"),
|
|
LLMEndpoint: env("LLM_ENDPOINT", "http://localhost:8000"),
|
|
LLMModel: env("LLM_MODEL", ""),
|
|
LLMChatModel: env("LLM_CHAT_MODEL", ""),
|
|
LLMTimeout: envDuration("LLM_TIMEOUT", 30*time.Second),
|
|
|
|
TTSEndpoint: env("TTS_ENDPOINT", ""),
|
|
TTSEndpointZH: env("TTS_ENDPOINT_ZH", ""),
|
|
TTSVoiceEN: env("TTS_VOICE_EN", "en_US-amy-medium"),
|
|
TTSVoiceZH: env("TTS_VOICE_ZH", "zh_CN-huayan-medium"),
|
|
TTSCacheDir: env("TTS_CACHE_DIR", "./data/tts"),
|
|
TTSTimeout: envDuration("TTS_TIMEOUT", 15*time.Second),
|
|
TTSFormat: env("TTS_AUDIO_FORMAT", "mp3"),
|
|
|
|
AuthentikURL: env("AUTHENTIK_URL", ""),
|
|
AuthentikClientID: env("AUTHENTIK_CLIENT_ID", ""),
|
|
AuthentikClientSecret: env("AUTHENTIK_CLIENT_SECRET", ""),
|
|
SessionSecret: env("SESSION_SECRET", "dev-insecure-secret-change-me"),
|
|
}
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envDuration(key string, fallback time.Duration) time.Duration {
|
|
if v := os.Getenv(key); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
return fallback
|
|
}
|