327 lines
12 KiB
Go
327 lines
12 KiB
Go
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("Сервер остановлен")
|
||
}
|