package main import ( crypto_tls "crypto/tls" "embed" "fmt" "io/fs" "log/slog" "os" "os/signal" "syscall" "github.com/imbytecat/voicepaste/internal/asr" "github.com/imbytecat/voicepaste/internal/config" "github.com/imbytecat/voicepaste/internal/paste" "github.com/imbytecat/voicepaste/internal/server" vpTLS "github.com/imbytecat/voicepaste/internal/tls" "github.com/imbytecat/voicepaste/internal/ws" ) //go:embed all:web var webFS embed.FS var version = "dev" func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelInfo, }))) slog.Info("VoicePaste", "version", version) // Load config cfg, err := config.Load("") if err != nil { slog.Error("failed to load config", "error", err) os.Exit(1) } // Start config hot-reload watcher config.WatchAndReload("") // Initialize clipboard if err := paste.Init(); err != nil { slog.Warn("clipboard init failed, paste will be unavailable", "err", err) } // Detect LAN IP lanIP, err := server.GetLANIP() if err != nil { slog.Error("failed to detect LAN IP", "error", err) os.Exit(1) } // Generate auth token token := server.GenerateToken() // TLS setup var tlsResult *vpTLS.Result scheme := "http" host := lanIP if cfg.Server.TLSAuto { var err error tlsResult, err = vpTLS.GetTLSConfig(lanIP) if err != nil { slog.Error("TLS setup failed", "error", err) os.Exit(1) } scheme = "https" host = tlsResult.Host } // Build URL url := fmt.Sprintf("%s://%s:%d/?token=%s", scheme, host, cfg.Server.Port, token) // Print connection info fmt.Println() fmt.Println("╔══════════════════════════════════════╗") fmt.Println("║ VoicePaste Ready ║") fmt.Println("╚══════════════════════════════════════╝") fmt.Println() fmt.Printf(" URL: %s\n", url) if tlsResult != nil && tlsResult.AnyIP { fmt.Println(" TLS: AnyIP (browser-trusted)") } else if cfg.Server.TLSAuto { fmt.Println(" TLS: self-signed (browser will warn)") } fmt.Println() printQRCode(url) fmt.Println() fmt.Println(" Scan QR code with your phone to connect.") fmt.Println(" Press Ctrl+C to stop.") fmt.Println() // Create and start server webContent, _ := fs.Sub(webFS, "web") var serverTLSCfg *crypto_tls.Config if tlsResult != nil { serverTLSCfg = tlsResult.Config } srv := server.New(token, lanIP, webContent, serverTLSCfg) // Build ASR factory from config asrCfg := asr.Config{ AppKey: cfg.Doubao.AppKey, AccessKey: cfg.Doubao.AccessKey, ResourceID: cfg.Doubao.ResourceID, } asrFactory := func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) { client, err := asr.Dial(asrCfg, resultCh) if err != nil { return nil, nil, err } sendAudio := func(pcm []byte) { if err := client.SendAudio(pcm, false); err != nil { slog.Warn("send audio to asr", "err", err) } } cleanup := func() { // Send last empty frame to signal end _ = client.SendAudio(nil, true) client.Close() } return sendAudio, cleanup, nil } // Register WebSocket handler wsHandler := ws.NewHandler(token, paste.Paste, asrFactory) wsHandler.Register(srv.App()) // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh slog.Info("shutting down...") srv.Shutdown() }() if err := srv.Start(); err != nil { slog.Error("server error", "error", err) os.Exit(1) } }