145 lines
3.7 KiB
Go
145 lines
3.7 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)
|
||
}
|
||
|
||
// Read token from config (empty = no auth required)
|
||
token := cfg.Security.Token
|
||
|
||
// 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
|
||
var url string
|
||
if token != "" {
|
||
url = fmt.Sprintf("%s://%s:%d/?token=%s", scheme, host, cfg.Server.Port, token)
|
||
} else {
|
||
url = fmt.Sprintf("%s://%s:%d/", scheme, host, cfg.Server.Port)
|
||
}
|
||
// Print connection info
|
||
fmt.Println()
|
||
fmt.Println("╔══════════════════════════════════════╗")
|
||
fmt.Println("║ VoicePaste 就绪 ║")
|
||
fmt.Println("╚══════════════════════════════════════╝")
|
||
fmt.Println()
|
||
fmt.Printf(" 地址: %s\n", url)
|
||
if tlsResult != nil && tlsResult.AnyIP {
|
||
fmt.Println(" 证书: AnyIP(浏览器信任)")
|
||
} else if cfg.Server.TLSAuto {
|
||
fmt.Println(" 证书: 自签名(浏览器会警告)")
|
||
}
|
||
if token != "" {
|
||
fmt.Println(" 认证: 已启用")
|
||
} else {
|
||
fmt.Println(" 认证: 未启用(无需 token)")
|
||
}
|
||
fmt.Println()
|
||
printQRCode(url)
|
||
fmt.Println()
|
||
fmt.Println(" 用手机扫描二维码连接")
|
||
fmt.Println(" 按 Ctrl+C 停止服务")
|
||
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() {
|
||
client.Finish()
|
||
}
|
||
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)
|
||
}
|
||
} |