before stt adding

This commit is contained in:
2025-11-28 17:43:00 +06:00
parent fccafad6de
commit f933c315e8
17 changed files with 10002 additions and 672 deletions

384
main.go
View File

@@ -5,7 +5,6 @@ import (
"crypto/tls"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"log"
@@ -29,11 +28,42 @@ 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)
}
// Получение конфигурации из переменных окружения
port := getEnv("GO_SPEECH_PORT", "8443")
// Определение режима работы: 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")
@@ -68,45 +98,71 @@ func main() {
logger.Info("=== Запуск Go Speech TTS сервера ===")
logger.Debug("Режим отладки: ВКЛЮЧЕН")
logger.Debug("Конфигурация:")
logger.Debug(" PORT: %s", port)
logger.Debug(" CERT_FILE: %s", certFile)
logger.Debug(" KEY_FILE: %s", keyFile)
logger.Debug(" PIPER_PATH: %s", piperPath)
logger.Debug(" FFMPEG_PATH: %s", ffmpegPath)
logger.Debug(" MODEL_DIR: %s", modelDir)
logger.Debug(" CACHE_DIR: %s", cacheDir)
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)
// Проверка наличия сертификатов
logger.Debug("Проверка SSL сертификатов...")
if _, err := os.Stat(certFile); os.IsNotExist(err) {
logger.Error("SSL сертификат не найден: %s", certFile)
log.Fatalf("SSL сертификат не найден: %s. Создайте сертификаты или укажите путь через CERT_FILE", certFile)
}
logger.Debug("SSL сертификат найден: %s", certFile)
var tlsConfig *tls.Config
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
logger.Error("SSL ключ не найден: %s", keyFile)
log.Fatalf("SSL ключ не найден: %s. Создайте ключ или укажите путь через KEY_FILE", keyFile)
}
logger.Debug("SSL ключ найден: %s", keyFile)
// Обработка TLS конфигурации только если включен HTTPS
if useTLS {
logger.Debug("Режим HTTPS включен, проверка сертификатов...")
// Загрузка 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 сертификаты успешно загружены")
// Проверка наличия сертификатов
certExists := false
keyExists := false
// Настройка TLS конфигурации
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
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 отключен)")
}
// Статические файлы для фронтенда
@@ -137,6 +193,13 @@ func main() {
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) {
@@ -152,8 +215,20 @@ func main() {
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")
io.Copy(w, indexFile)
w.Write([]byte(indexContentStr))
})
logger.Debug(" GET /go-speech/front - веб-интерфейс")
@@ -161,6 +236,15 @@ func main() {
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 - документация")
@@ -168,16 +252,25 @@ func main() {
server := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: loggingMiddleware(mux),
TLSConfig: tlsConfig,
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() {
@@ -190,17 +283,22 @@ func main() {
}
logger.Debug("IPv4 listener успешно создан: %v", listener.Addr())
// Создаем TLS listener
logger.Debug("Создание TLS listener...")
tlsListener := tls.NewListener(listener, tlsConfig)
logger.Debug("TLS listener успешно создан")
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("Сервер запущен на https://0.0.0.0:%s (IPv4)", port)
logger.Info("Простой веб-интерфейс доступен на https://localhost:%s/go-speech/front", port)
logger.Info("Справка доступна по пути https://localhost:%s/go-speech/help", port)
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(tlsListener); err != nil && err != http.ErrServerClosed {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
logger.Error("Ошибка запуска сервера: %v", err)
log.Fatalf("Ошибка запуска сервера: %v", err)
}
@@ -226,203 +324,3 @@ func main() {
logger.Info("Сервер остановлен")
}
// loggingMiddleware добавляет логирование запросов
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
logger.Debug("Входящий запрос: %s %s от %s", r.Method, r.URL.Path, r.RemoteAddr)
logger.Debug(" User-Agent: %s", r.UserAgent())
logger.Debug(" Content-Type: %s", r.Header.Get("Content-Type"))
logger.Debug(" Content-Length: %s", r.Header.Get("Content-Length"))
next.ServeHTTP(w, r)
duration := time.Since(start)
logger.Info("%s %s %v", r.Method, r.URL.Path, duration)
logger.Debug("Запрос обработан за %v", duration)
})
}
// getEnv получает переменную окружения или возвращает значение по умолчанию
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// GetModelPath формирует путь к модели на основе выбранного голоса
func GetModelPath(modelDir, voice string) string {
// Если указан полный путь через MODEL_PATH, используем его
if modelPath := os.Getenv("MODEL_PATH"); modelPath != "" {
return modelPath
}
// Формируем путь к модели на основе голоса
modelFile := fmt.Sprintf("ru_RU-%s-medium.onnx", voice)
return filepath.Join(modelDir, modelFile)
}
// handleHelp рендерит README.md в HTML
func handleHelp(w http.ResponseWriter, r *http.Request) {
logger.Debug("GET /go-speech/help - отображение документации")
// Простой рендеринг markdown в HTML
html := markdownToHTML(readmeContent)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(html))
}
// markdownToHTML конвертирует markdown в простой HTML
func markdownToHTML(md string) string {
html := `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Speech - Документация</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
h1, h2, h3 { color: #667eea; margin-top: 30px; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; }
pre { background: #f4f4f4; padding: 15px; border-radius: 8px; overflow-x: auto; }
pre code { background: none; padding: 0; }
a { color: #667eea; text-decoration: none; }
a:hover { text-decoration: underline; }
blockquote { border-left: 4px solid #667eea; padding-left: 20px; margin-left: 0; color: #666; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #667eea; color: white; }
</style>
</head>
<body>
`
// Простая конвертация markdown в HTML
lines := strings.Split(md, "\n")
inCodeBlock := false
inList := false
var codeBlock strings.Builder
for i, line := range lines {
line = strings.TrimRight(line, "\r")
trimmedLine := strings.TrimSpace(line)
// Обработка блоков кода
if strings.HasPrefix(trimmedLine, "```") {
if inCodeBlock {
// Закрываем блок кода
html += "<pre><code>" + template.HTMLEscapeString(codeBlock.String()) + "</code></pre>\n"
codeBlock.Reset()
inCodeBlock = false
} else {
// Открываем блок кода
inCodeBlock = true
}
if inList {
html += "</ul>\n"
inList = false
}
continue
}
if inCodeBlock {
codeBlock.WriteString(line + "\n")
continue
}
// Заголовки
if strings.HasPrefix(trimmedLine, "# ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h1>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "# ")) + "</h1>\n"
continue
}
if strings.HasPrefix(trimmedLine, "## ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h2>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "## ")) + "</h2>\n"
continue
}
if strings.HasPrefix(trimmedLine, "### ") {
if inList {
html += "</ul>\n"
inList = false
}
html += "<h3>" + template.HTMLEscapeString(strings.TrimPrefix(trimmedLine, "### ")) + "</h3>\n"
continue
}
// Списки
if strings.HasPrefix(trimmedLine, "- ") {
if !inList {
html += "<ul>\n"
inList = true
}
content := strings.TrimPrefix(trimmedLine, "- ")
// Обработка inline кода в списках
content = processInlineCode(content)
html += "<li>" + content + "</li>\n"
continue
}
// Закрываем список если он был открыт
if inList && trimmedLine == "" {
html += "</ul>\n"
inList = false
}
// Пустые строки
if trimmedLine == "" {
if i < len(lines)-1 {
html += "<br>\n"
}
continue
}
// Обычный текст
content := processInlineCode(template.HTMLEscapeString(trimmedLine))
html += "<p>" + content + "</p>\n"
}
// Закрываем открытые теги
if inList {
html += "</ul>\n"
}
html += `</body>
</html>`
return html
}
// processInlineCode обрабатывает inline код в markdown
func processInlineCode(text string) string {
// Простая обработка inline кода `code`
parts := strings.Split(text, "`")
result := strings.Builder{}
for i, part := range parts {
if i%2 == 0 {
result.WriteString(part)
} else {
result.WriteString("<code>")
result.WriteString(part)
result.WriteString("</code>")
}
}
return result.String()
}