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