speech.go
1707 bytes
1package server
2
3import (
4 "context"
5 "errors"
6 "sync"
7
8 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/stt"
9 "git.theedgeofrage.com/TheEdgeOfRage/kaiwari/internal/tts"
10)
11
12// remoteInput adapts the uploaded WAV of one spoken turn to game.SpeechInput.
13// Begin is a no-op; End transcribes the bytes stashed with SetWAV.
14type remoteInput struct {
15 asr *stt.ASRClient
16
17 mu sync.Mutex
18 wav []byte
19}
20
21func (r *remoteInput) Begin(ctx context.Context) error { return nil }
22
23// SetWAV stashes the WAV uploaded for this turn.
24func (r *remoteInput) SetWAV(wav []byte) {
25 r.mu.Lock()
26 r.wav = wav
27 r.mu.Unlock()
28}
29
30func (r *remoteInput) End(ctx context.Context) (string, error) {
31 r.mu.Lock()
32 wav := r.wav
33 r.wav = nil
34 r.mu.Unlock()
35 if len(wav) == 0 {
36 return "", errors.New("speech: no audio uploaded")
37 }
38 raw, err := r.asr.TranscribeBytes(ctx, wav)
39 if err != nil {
40 return "", err
41 }
42 return raw, nil
43}
44
45// bufferedOutput adapts the TTS client to game.SpeechOutput without playback:
46// Speak synthesizes into memory and TakeLast drains the last clip.
47type bufferedOutput struct {
48 client *tts.Client
49
50 mu sync.Mutex
51 last []byte
52}
53
54func (b *bufferedOutput) Speak(ctx context.Context, kana, gender string) error {
55 wav, err := b.client.Speech(ctx, kana, tts.VoiceForGender(gender))
56 if err != nil {
57 return err
58 }
59 b.mu.Lock()
60 cp := make([]byte, len(wav))
61 copy(cp, wav)
62 b.last = cp
63 b.mu.Unlock()
64 return nil
65}
66
67// TakeLast returns the last synthesized clip and clears it.
68func (b *bufferedOutput) TakeLast() []byte {
69 b.mu.Lock()
70 wav := b.last
71 b.last = nil
72 b.mu.Unlock()
73 return wav
74}
75
76// Replay is a no-op: replay is client-side on iOS.
77func (b *bufferedOutput) Replay() error { return nil }