Files
go-speech/main.go
2025-11-28 17:43:00 +06:00

327 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"crypto/tls"
"embed"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"go-speech/handlers"
"go-speech/internal/cache"
"go-speech/internal/logger"
)
//go:embed static
var staticFiles embed.FS
//go:embed README.md
var readmeContent string
//go:embed VERSION.txt
var versionContent string
// getVersion возвращает версию приложения
func getVersion() string {
if versionContent != "" {
return strings.TrimSpace(versionContent)
}
return "unknown"
}
func main() {
// Обработка флага -V для вывода версии
if len(os.Args) > 1 && (os.Args[1] == "-V" || os.Args[1] == "--version") {
fmt.Printf("Go Speech TTS версия: %s\n", getVersion())
os.Exit(0)
}
// Получение конфигурации из переменных окружения
// Определение режима работы: HTTPS или HTTP
// GO_SPEECH_TLS=true включает HTTPS, по умолчанию (если не задано) - HTTP
useTLS := getEnv("GO_SPEECH_TLS", "false") == "true"
port := "8443"
if useTLS {
port = getEnv("GO_SPEECH_PORT", "8443")
} else {
port = getEnv("GO_SPEECH_PORT", "8080")
}
certFile := getEnv("GO_SPEECH_CERT_FILE", "certs/server.crt")
keyFile := getEnv("GO_SPEECH_KEY_FILE", "certs/server.key")
caCertFile := getEnv("GO_SPEECH_CA_CERT", "") // Путь к CA сертификату (опционально)
tlsDomains := getEnv("GO_SPEECH_TLS_DOMAINS", "") // Домены для сертификата через запятую
piperPath := getEnv("GO_SPEECH_PIPER_PATH", "/usr/local/bin/piper")
ffmpegPath := getEnv("GO_SPEECH_FFMPEG_PATH", "/usr/bin/ffmpeg")
// Получение пути к директории с моделями
modelDir := getEnv("GO_SPEECH_MODEL_DIR", "models")
// Определение директории кэша (рядом с исполняемым файлом)
execPath, err := os.Executable()
if err != nil {
logger.Warn("Не удалось определить путь к исполняемому файлу: %v", err)
execPath = "."
}
execDir := filepath.Dir(execPath)
cacheDir := filepath.Join(execDir, "cache")
logger.Debug("Директория исполняемого файла: %s", execDir)
logger.Debug("Директория кэша: %s", cacheDir)
// Инициализация кэша
logger.Debug("Инициализация кэша...")
cacheInstance, err := cache.NewCache(cacheDir)
if err != nil {
logger.Error("Ошибка инициализации кэша: %v", err)
log.Fatalf("Ошибка инициализации кэша: %v", err)
}
// Запуск фоновой горутины для очистки кэша (каждые 5 минут, удаляем файлы старше 3 дней)
cacheCleanupInterval := 5 * time.Minute
cacheMaxAge := 3 * 24 * time.Hour
cacheInstance.StartCleanupRoutine(cacheCleanupInterval, cacheMaxAge)
// Детальное логирование конфигурации
logger.Info("=== Запуск Go Speech TTS сервера ===")
logger.Debug("Режим отладки: ВКЛЮЧЕН")
logger.Debug("Конфигурация:")
logger.Debug(" GO_SPEECH_PORT: %s", port)
logger.Debug(" GO_SPEECH_TLS: %v (HTTPS: %v)", getEnv("GO_SPEECH_TLS", "не задано"), useTLS)
logger.Debug(" GO_SPEECH_CERT_FILE: %s", certFile)
logger.Debug(" GO_SPEECH_KEY_FILE: %s", keyFile)
if caCertFile != "" {
logger.Debug(" GO_SPEECH_CA_CERT: %s", caCertFile)
}
if tlsDomains != "" {
logger.Debug(" GO_SPEECH_TLS_DOMAINS: %s", tlsDomains)
}
logger.Debug(" GO_SPEECH_PIPER_PATH: %s", piperPath)
logger.Debug(" GO_SPEECH_FFMPEG_PATH: %s", ffmpegPath)
logger.Debug(" GO_SPEECH_MODEL_DIR: %s", modelDir)
logger.Debug(" GO_SPEECH_CACHE_DIR: %s", cacheDir)
logger.Debug(" GO_SPEECH_VOICE: %s", getEnv("GO_SPEECH_VOICE", "ruslan"))
logger.Debug(" GO_SPEECH_MODE: %s", getEnv("GO_SPEECH_MODE", "debug"))
logger.Info("Директория с моделями: %s", modelDir)
logger.Info("Директория кэша: %s", cacheDir)
var tlsConfig *tls.Config
// Обработка TLS конфигурации только если включен HTTPS
if useTLS {
logger.Debug("Режим HTTPS включен, проверка сертификатов...")
// Проверка наличия сертификатов
certExists := false
keyExists := false
if _, err := os.Stat(certFile); err == nil {
certExists = true
logger.Debug("SSL сертификат найден: %s", certFile)
}
if _, err := os.Stat(keyFile); err == nil {
keyExists = true
logger.Debug("SSL ключ найден: %s", keyFile)
}
// Если сертификаты отсутствуют - генерируем их
if !certExists || !keyExists {
logger.Info("Сертификаты не найдены, начинаем генерацию...")
if err := generateCertificate(certFile, keyFile, caCertFile, tlsDomains); err != nil {
logger.Error("Ошибка генерации сертификатов: %v", err)
log.Fatalf("Ошибка генерации сертификатов: %v", err)
}
logger.Info("Сертификаты успешно сгенерированы: %s, %s", certFile, keyFile)
}
// Загрузка TLS сертификатов
logger.Debug("Загрузка TLS сертификатов...")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
logger.Error("Ошибка загрузки TLS сертификатов: %v", err)
log.Fatalf("Ошибка загрузки TLS сертификатов: %v", err)
}
logger.Debug("TLS сертификаты успешно загружены")
// Настройка TLS конфигурации
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
} else {
logger.Info("Режим HTTP (TLS отключен)")
}
// Статические файлы для фронтенда
logger.Debug("Загрузка статических файлов...")
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
logger.Error("Ошибка загрузки статических файлов: %v", err)
log.Fatalf("Ошибка загрузки статических файлов: %v", err)
}
logger.Debug("Статические файлы успешно загружены")
// Создание HTTP сервера
mux := http.NewServeMux()
// Инициализация TTS обработчика
logger.Debug("Инициализация TTS обработчика...")
ttsHandler := handlers.NewTTSHandler(piperPath, modelDir, ffmpegPath, cacheInstance)
logger.Debug("Регистрация маршрутов:")
// API endpoints
mux.HandleFunc("/go-speech/api/v1/tts", ttsHandler.HandleTTS)
logger.Debug(" POST /go-speech/api/v1/tts - синтез речи")
mux.HandleFunc("/go-speech/api/v1/healthz", ttsHandler.HandleHealthz)
logger.Debug(" GET /go-speech/api/v1/healthz - проверка TTS")
mux.HandleFunc("/go-speech/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
logger.Debug("GET /go-speech/api/v1/health - проверка работоспособности")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
logger.Debug(" GET /go-speech/api/v1/health - проверка работоспособности")
mux.HandleFunc("/go-speech/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
logger.Debug("GET /go-speech/api/v1/version - получение версии")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"version":"%s"}`, getVersion())
})
logger.Debug(" GET /go-speech/api/v1/version - получение версии")
// Веб-интерфейс
mux.HandleFunc("/go-speech/front", func(w http.ResponseWriter, r *http.Request) {
logger.Debug("GET /go-speech/front - веб-интерфейс")
if r.URL.Path != "/go-speech/front" {
http.Redirect(w, r, "/go-speech/front", http.StatusMovedPermanently)
return
}
indexFile, err := staticFS.Open("index.html")
if err != nil {
logger.Error("Ошибка открытия index.html: %v", err)
http.Error(w, "Фронтенд не найден", http.StatusNotFound)
return
}
defer indexFile.Close()
// Читаем содержимое index.html
indexContent, err := io.ReadAll(indexFile)
if err != nil {
logger.Error("Ошибка чтения index.html: %v", err)
http.Error(w, "Ошибка чтения фронтенда", http.StatusInternalServerError)
return
}
// Заменяем плейсхолдер версии
indexContentStr := strings.ReplaceAll(string(indexContent), "{{VERSION}}", getVersion())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(indexContentStr))
})
logger.Debug(" GET /go-speech/front - веб-интерфейс")
// Обработчик для статических ресурсов фронтенда
mux.Handle("/go-speech/front/", http.StripPrefix("/go-speech/front/", http.FileServer(http.FS(staticFS))))
logger.Debug(" GET /go-speech/front/* - статические ресурсы")
// Обработчик для шрифтов
fontsFS, err := fs.Sub(staticFiles, "static/fonts")
if err == nil {
mux.Handle("/go-speech/fonts/", http.StripPrefix("/go-speech/fonts/", http.FileServer(http.FS(fontsFS))))
logger.Debug(" GET /go-speech/fonts/* - шрифты")
} else {
logger.Warn("Не удалось загрузить шрифты: %v", err)
}
// Помощь - рендеринг README.md
mux.HandleFunc("/go-speech/help", handleHelp)
logger.Debug(" GET /go-speech/help - документация")
server := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: loggingMiddleware(mux),
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Добавляем TLS конфигурацию только если включен HTTPS
if useTLS {
server.TLSConfig = tlsConfig
}
logger.Debug("Настройки HTTP сервера:")
logger.Debug(" ReadTimeout: %v", server.ReadTimeout)
logger.Debug(" WriteTimeout: %v", server.WriteTimeout)
logger.Debug(" IdleTimeout: %v", server.IdleTimeout)
if useTLS {
logger.Debug(" TLS: включен")
} else {
logger.Debug(" TLS: отключен")
}
// Graceful shutdown
go func() {
// Создаем явный IPv4 listener
logger.Debug("Создание IPv4 listener на 0.0.0.0:%s...", port)
listener, err := net.Listen("tcp4", "0.0.0.0:"+port)
if err != nil {
logger.Error("Ошибка создания IPv4 listener: %v", err)
log.Fatalf("Ошибка создания IPv4 listener: %v", err)
}
logger.Debug("IPv4 listener успешно создан: %v", listener.Addr())
protocol := "http"
if useTLS {
protocol = "https"
// Создаем TLS listener
logger.Debug("Создание TLS listener...")
tlsListener := tls.NewListener(listener, tlsConfig)
logger.Debug("TLS listener успешно создан")
listener = tlsListener
}
logger.Info("Сервер запущен на %s://0.0.0.0:%s (IPv4)", protocol, port)
logger.Info("Простой веб-интерфейс доступен на %s://localhost:%s/go-speech/front", protocol, port)
logger.Info("Справка доступна по пути %s://localhost:%s/go-speech/help", protocol, port)
logger.Debug("Запуск server.Serve()...")
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
logger.Error("Ошибка запуска сервера: %v", err)
log.Fatalf("Ошибка запуска сервера: %v", err)
}
logger.Debug("server.Serve() завершился")
}()
// Ожидание сигнала для graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
logger.Debug("Ожидание сигнала для остановки (SIGINT/SIGTERM)...")
sig := <-quit
logger.Info("Получен сигнал: %v", sig)
logger.Info("Остановка сервера...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
logger.Debug("Graceful shutdown с таймаутом 5 секунд...")
if err := server.Shutdown(ctx); err != nil {
logger.Error("Ошибка при остановке сервера: %v", err)
log.Fatalf("Ошибка при остановке сервера: %v", err)
}
logger.Info("Сервер остановлен")
}