main.go
2465 bytes
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/signal"
8 "syscall"
9 "time"
10
11 "github.com/charmbracelet/bubbletea"
12
13 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/adapters"
14 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/bootstrap"
15 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/config"
16 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/game"
17 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/stt"
18 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/tts"
19 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/ui"
20)
21
22// recordCap bounds a single push-to-talk capture. It is passed to the recorder
23// (which enforces it) and to the UI (which shows it as the cap indicator).
24const recordCap = 15 * time.Second
25
26// The capture command is fixed for this deployment; it is not user-configurable.
27const recordCommand = "arecord -r 16000 -f S16_LE -c 1"
28
29func main() {
30 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
31 defer stop()
32
33 cfg := config.ParseConfig()
34
35 svc, err := bootstrap.New(ctx, cfg)
36 if err != nil {
37 fatalf("%v", err)
38 }
39 defer svc.Close()
40
41 state := game.NewState(svc.Brief)
42 orch, speechIn := buildOrchestrator(svc, state)
43 defer speechIn.Close()
44
45 m := ui.NewModel(state, orch, int(recordCap.Seconds()))
46 p := tea.NewProgram(m)
47 go func() {
48 <-ctx.Done()
49 p.Quit()
50 }()
51 if _, err := p.Run(); err != nil {
52 svc.Close()
53 fatalf("%v", err)
54 }
55}
56
57func buildOrchestrator(svc *bootstrap.Services, state *game.State) (*game.Orchestrator, *adapters.SpeechInput) {
58 gameModel := &adapters.GameModel{Client: svc.Game}
59 compactor := &adapters.Compactor{Client: svc.Compaction}
60 judgeModel := &adapters.JudgeModel{Client: svc.Judge}
61 sheetModel := &adapters.SheetModel{Client: svc.Sheet}
62 askModel := &adapters.ScratchModel{Client: svc.Scratch}
63 flashModel := &adapters.FlashcardModel{Client: svc.Scratch}
64
65 speechIn := &adapters.SpeechInput{
66 Recorder: stt.NewRecorder(recordCommand, recordCap),
67 ASR: stt.NewASRClient(svc.Cfg.AudioConfig.AudioBaseURL, svc.HTTP),
68 }
69 speechOut := &adapters.SpeechOutput{
70 Client: tts.NewClient(svc.Cfg.AudioConfig.AudioBaseURL, svc.HTTP),
71 Player: tts.NewPlayer(),
72 }
73
74 orch := game.NewOrchestrator(state, gameModel, judgeModel, compactor, sheetModel, askModel, flashModel, speechIn, speechOut)
75 return orch, speechIn
76}
77
78func fatalf(format string, args ...any) {
79 fmt.Fprintf(os.Stderr, "kaiwari: "+format+"\n", args...)
80 os.Exit(1)
81}