137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
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)
|
|
}
|
|
} |