Skip to content

Commit b84acda

Browse files
committed
Add Chromium to websocket proxy.
1 parent bfe8fb9 commit b84acda

18 files changed

+1318
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ models/*
2424
third_party/whisper.cpp
2525
.env
2626
dist
27+
28+
/.vscode

cmd/crtowebsocket/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Chromium to WebSocket
2+
3+
This is a simple HTTP server that listens for WebSpeech connections from a
4+
Chromium browser, converts audio data to WAV format, streams it to a WebSocket
5+
client, receives text data from the WebSocket client, and sends it back to the
6+
Chromium browser.
7+
8+
## Usage
9+
10+
```bash
11+
go run .
12+
```
13+
14+
## Building
15+
16+
```bash
17+
go build -o crtowebsocket .
18+
```
19+
20+
## Running
21+
22+
```bash
23+
./crtowebocket
24+
```
25+
26+
## Building for release
27+
28+
```bash
29+
go build -o crtowebsocket .
30+
```
31+
32+
## Running for release
33+
34+
```bash
35+
./crtowebsocket
36+
```

cmd/crtowebsocket/main.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"strconv"
7+
"time"
8+
9+
"github.com/brave-experiments/go-stt/cr_api_websocket_proxy"
10+
"github.com/rs/zerolog"
11+
"github.com/rs/zerolog/log"
12+
"github.com/urfave/cli/v2"
13+
)
14+
15+
// Configuration for the remote WebSocket STT service
16+
const (
17+
version = "1"
18+
defaultListenAddress = "127.0.0.1:8090"
19+
defaultWebsocketURL = "ws://127.0.0.1:8080/api-speech-wss/"
20+
)
21+
22+
func main() {
23+
zerolog.SetGlobalLevel(zerolog.InfoLevel)
24+
log.Logger = log.Output(
25+
zerolog.ConsoleWriter{
26+
Out: os.Stderr,
27+
NoColor: true,
28+
},
29+
)
30+
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
31+
short := file
32+
for i := len(file) - 1; i > 0; i-- {
33+
if file[i] == '/' {
34+
short = file[i+1:]
35+
break
36+
}
37+
}
38+
file = short
39+
return file + ":" + strconv.Itoa(line)
40+
}
41+
42+
zerolog.SetGlobalLevel(zerolog.DebugLevel)
43+
log.Logger = log.With().Caller().Logger()
44+
45+
app := cli.NewApp()
46+
app.Name = "Chromium WebSpeech API Endpoint to WebSocket proxy"
47+
app.Version = version
48+
app.Flags = []cli.Flag{
49+
&cli.StringFlag{
50+
Name: "listen-address",
51+
Value: defaultListenAddress,
52+
},
53+
&cli.StringFlag{
54+
Name: "websocket-url",
55+
Value: defaultWebsocketURL,
56+
},
57+
&cli.DurationFlag{
58+
Name: "timeout",
59+
Value: 60 * time.Second,
60+
},
61+
&cli.BoolFlag{
62+
Name: "try-to-finalize-text",
63+
Value: false,
64+
},
65+
}
66+
app.Action = run
67+
68+
if err := app.Run(os.Args); err != nil {
69+
log.Fatal().Err(err)
70+
}
71+
}
72+
73+
func run(c *cli.Context) error {
74+
// Create a configuration struct
75+
config := &cr_api_websocket_proxy.HandlerConfig{
76+
WebsocketURL: c.String("websocket-url"),
77+
Timeout: c.Duration("timeout"),
78+
TryToFinalizeText: c.Bool("try-to-finalize-text"),
79+
}
80+
81+
// Create a handler instance with the config
82+
handler := cr_api_websocket_proxy.NewHandler(config)
83+
84+
// Register handlers that have access to the config
85+
http.HandleFunc("/up", handler.HandleUpstreamRequest)
86+
http.HandleFunc("/down", handler.HandleDownstreamRequest)
87+
88+
http.ListenAndServe(c.String("listen-address"), nil)
89+
90+
return nil
91+
}

cmd/crtowebsocket/pprof.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build pprof
2+
3+
package main
4+
5+
import (
6+
_ "net/http/pprof"
7+
)

cr_api_websocket_proxy/audio.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cr_api_websocket_proxy
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"azul3d.org/engine/audio"
8+
"github.com/colega/zeropool"
9+
10+
// Add flac decoder for decoding incoming audio
11+
_ "azul3d.org/engine/audio/flac"
12+
)
13+
14+
const expectedSampleRate = 16000
15+
16+
const (
17+
samplesPerChunk = expectedSampleRate / 1000 * 20 // 20ms
18+
bytesPerChunk = samplesPerChunk * 2
19+
)
20+
21+
var audioSamplesBufferPool = zeropool.New(
22+
func() audio.Int16 {
23+
return make(
24+
audio.Int16,
25+
samplesPerChunk,
26+
)
27+
},
28+
)
29+
30+
var audioBytesBufferPool = zeropool.New(
31+
func() []byte {
32+
return make(
33+
[]byte,
34+
bytesPerChunk,
35+
)
36+
},
37+
)
38+
39+
func NewAudioDecoder(req *http.Request) (audio.Decoder, error) {
40+
dec, _, err := audio.NewDecoder(req.Body)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
// Ensure we're working with the correct sample rate
46+
if dec.Config().SampleRate != expectedSampleRate {
47+
return nil, fmt.Errorf("unexpected sample rate: %d", dec.Config().SampleRate)
48+
}
49+
50+
return dec, nil
51+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cr_api_websocket_proxy
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"os"
8+
"testing"
9+
10+
"azul3d.org/engine/audio"
11+
)
12+
13+
func TestAudioBufferPools(t *testing.T) {
14+
t.Run("samples buffer pool", func(t *testing.T) {
15+
samples := audioSamplesBufferPool.Get()
16+
if samples == nil {
17+
t.Error("expected non-nil samples buffer")
18+
}
19+
if got := len(samples); got != samplesPerChunk {
20+
t.Errorf("samples buffer length = %v, want %v", got, samplesPerChunk)
21+
}
22+
23+
audioSamplesBufferPool.Put(samples)
24+
})
25+
26+
t.Run("bytes buffer pool", func(t *testing.T) {
27+
buffer := audioBytesBufferPool.Get()
28+
if buffer == nil {
29+
t.Error("expected non-nil bytes buffer")
30+
}
31+
if got := len(buffer); got != bytesPerChunk {
32+
t.Errorf("bytes buffer length = %v, want %v", got, bytesPerChunk)
33+
}
34+
35+
audioBytesBufferPool.Put(buffer)
36+
})
37+
}
38+
39+
type mockBody struct {
40+
*bytes.Buffer
41+
}
42+
43+
func (m mockBody) Close() error {
44+
return nil
45+
}
46+
47+
func TestFlacDecoder_InvalidData(t *testing.T) {
48+
t.Run("invalid audio data", func(t *testing.T) {
49+
req := &http.Request{
50+
Body: mockBody{bytes.NewBuffer([]byte("invalid audio data"))},
51+
}
52+
53+
decoder, err := NewAudioDecoder(req)
54+
if err == nil {
55+
t.Error("expected error for invalid audio data")
56+
}
57+
if decoder != nil {
58+
t.Error("expected nil decoder for invalid audio data")
59+
}
60+
})
61+
}
62+
63+
func TestFlacDecoder_ValidData(t *testing.T) {
64+
req := &http.Request{
65+
Body: mockBody{bytes.NewBuffer(readTestFile(t, "testdata/16khz.flac"))},
66+
}
67+
68+
decoder, err := NewAudioDecoder(req)
69+
if err != nil {
70+
t.Fatalf("failed to create decoder: %v", err)
71+
}
72+
73+
// Verify decoder config
74+
config := decoder.Config()
75+
if config.SampleRate != expectedSampleRate {
76+
t.Errorf("sample rate = %v, want %v", config.SampleRate, expectedSampleRate)
77+
}
78+
79+
// Try reading some samples
80+
samples := make(audio.Int16, 1024)
81+
n, err := decoder.Read(samples)
82+
if err != nil && err != io.EOF {
83+
t.Errorf("failed to read samples: %v", err)
84+
}
85+
if n == 0 {
86+
t.Error("expected to read some samples")
87+
}
88+
}
89+
90+
func TestFlacDecoder_InvalidSampleRate(t *testing.T) {
91+
req := &http.Request{
92+
Body: mockBody{bytes.NewBuffer(readTestFile(t, "testdata/8khz.flac"))},
93+
}
94+
95+
decoder, err := NewAudioDecoder(req)
96+
if decoder != nil {
97+
t.Error("expected nil decoder for invalid sample rate")
98+
}
99+
if err == nil {
100+
t.Error("expected error for invalid sample rate")
101+
}
102+
}
103+
104+
// Helper to read test file contents
105+
func readTestFile(t *testing.T, path string) []byte {
106+
t.Helper()
107+
data, err := os.ReadFile(path)
108+
if err != nil {
109+
t.Fatalf("failed to read test file %s: %v", path, err)
110+
}
111+
return data
112+
}

0 commit comments

Comments
 (0)