Skip to content

Commit b0fc36d

Browse files
authored
Merge pull request #30 from mutablelogic/v1
Added a CLI interface to the server
2 parents 3fa5b41 + 3c73cf2 commit b0fc36d

25 files changed

+942
-505
lines changed

Makefile

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ BUILD_TAG := ${DOCKER_REGISTRY}/go-whisper-${OS}-${ARCH}:${VERSION}
1414
ROOT_PATH := $(CURDIR)
1515
BUILD_DIR := build
1616

17+
# Targets
18+
all: build server cli
19+
20+
# Make server
21+
server: mkdir go-tidy libwhisper libggml
22+
@echo "Building whisper-server"
23+
@CGO_CFLAGS="-I${ROOT_PATH}/third_party/whisper.cpp/include -I${ROOT_PATH}/third_party/whisper.cpp/ggml/include" \
24+
CGO_LDFLAGS="-L${ROOT_PATH}/third_party/whisper.cpp" \
25+
${GO} build -o ${BUILD_DIR}/whisper-server ./cmd/server
26+
27+
# Make cli
28+
cli: mkdir go-tidy
29+
@echo "Building whisper-cli"
30+
@CGO_CFLAGS="-I${ROOT_PATH}/third_party/whisper.cpp/include -I${ROOT_PATH}/third_party/whisper.cpp/ggml/include" \
31+
CGO_LDFLAGS="-L${ROOT_PATH}/third_party/whisper.cpp" \
32+
${GO} build -o ${BUILD_DIR}/whisper-cli ./cmd/cli
33+
1734
# Build docker container
1835
docker: docker-dep submodule
1936
@echo build docker image: ${BUILD_TAG} for ${OS}/${ARCH}
@@ -25,22 +42,16 @@ docker: docker-dep submodule
2542
--build-arg VERSION=${VERSION} \
2643
-f etc/Dockerfile.${ARCH} .
2744

28-
# Targets
29-
all: build server
30-
31-
# Make server
32-
server: mkdir go-tidy libwhisper libggml
33-
@echo "Building whisper-server"
34-
@CGO_CFLAGS="-I${ROOT_PATH}/third_party/whisper.cpp/include -I${ROOT_PATH}/third_party/whisper.cpp/ggml/include" \
35-
CGO_LDFLAGS="-L${ROOT_PATH}/third_party/whisper.cpp" \
36-
${GO} build -o ${BUILD_DIR}/whisper-server ./cmd/server
37-
3845
# Test whisper bindings
3946
test: go-tidy libwhisper libggml
40-
@echo "Running tests"
47+
@echo "Running tests (sys)"
4148
@CGO_CFLAGS="-I${ROOT_PATH}/third_party/whisper.cpp/include -I${ROOT_PATH}/third_party/whisper.cpp/ggml/include" \
4249
CGO_LDFLAGS="-L${ROOT_PATH}/third_party/whisper.cpp" \
4350
${GO} test -v ./sys/whisper/...
51+
@echo "Running tests (pkg)"
52+
@CGO_CFLAGS="-I${ROOT_PATH}/third_party/whisper.cpp/include -I${ROOT_PATH}/third_party/whisper.cpp/ggml/include" \
53+
CGO_LDFLAGS="-L${ROOT_PATH}/third_party/whisper.cpp" \
54+
${GO} test -v ./pkg/whisper/...
4455

4556
# Build whisper-static-library
4657
libwhisper: submodule

cmd/cli/delete.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
type DeleteCmd struct {
4+
Id string `arg:"" required:"" help:"Model Identifier" type:"string"`
5+
}
6+
7+
func (cmd *DeleteCmd) Run(ctx *Globals) error {
8+
err := ctx.api.DeleteModel(ctx.ctx, cmd.Id)
9+
if err != nil {
10+
return err
11+
}
12+
return nil
13+
}

cmd/cli/download.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import "fmt"
4+
5+
type DownloadCmd struct {
6+
Path string `arg:"" required:"" help:"Model Path" type:"string"`
7+
}
8+
9+
func (cmd *DownloadCmd) Run(ctx *Globals) error {
10+
type progress struct {
11+
Status string `json:"status" writer:",width:60"`
12+
Total int64 `json:"total,omitempty" writer:",right,width:12,"`
13+
Completed int64 `json:"completed,omitempty" writer:",right,width:12,"`
14+
Percent string `json:"percent,omitempty" writer:",width:8,right"`
15+
}
16+
model, err := ctx.api.DownloadModel(ctx.ctx, cmd.Path, func(status string, cur, total int64) {
17+
percent := ""
18+
if cur < total {
19+
percent = fmt.Sprintf("%.1f%%", float32(cur)*100/float32(total))
20+
}
21+
if status != "" {
22+
ctx.writer.Write(progress{
23+
Status: status,
24+
Completed: cur,
25+
Total: total,
26+
Percent: percent,
27+
})
28+
}
29+
})
30+
if err != nil {
31+
return err
32+
}
33+
return ctx.writer.Write(model)
34+
}

cmd/cli/main.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"syscall"
8+
9+
// Packages
10+
kong "github.com/alecthomas/kong"
11+
tablewriter "github.com/djthorpe/go-tablewriter"
12+
client "github.com/mutablelogic/go-client"
13+
ctx "github.com/mutablelogic/go-server/pkg/context"
14+
api "github.com/mutablelogic/go-whisper/pkg/client"
15+
)
16+
17+
type Globals struct {
18+
Debug bool `name:"debug" help:"Enable debug output"`
19+
Endpoint string `name:"endpoint" help:"HTTP endpoint for whisper service (set WHISPER_URL environment variable to use as default)" default:"${WHISPER_URL}"`
20+
21+
// Writer, client and context
22+
writer *tablewriter.Writer
23+
api *api.Client
24+
ctx context.Context
25+
}
26+
27+
type CLI struct {
28+
Globals
29+
Models ModelsCmd `cmd:"models" help:"List available models"`
30+
Delete DeleteCmd `cmd:"delete" help:"Delete a model"`
31+
Download DownloadCmd `cmd:"download" help:"Download a model"`
32+
Transcribe TranscribeCmd `cmd:"transcribe" help:"Transcribe a file"`
33+
Translate TranslateCmd `cmd:"translate" help:"Translate a file"`
34+
}
35+
36+
func main() {
37+
// The name of the executable
38+
name, err := os.Executable()
39+
if err != nil {
40+
panic(err)
41+
} else {
42+
name = filepath.Base(name)
43+
}
44+
45+
// Create a cli parser
46+
cli := CLI{}
47+
cmd := kong.Parse(&cli,
48+
kong.Name(name),
49+
kong.Description("speech transcription and translation service"),
50+
kong.UsageOnError(),
51+
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
52+
kong.Vars{
53+
"WHISPER_URL": endpointEnvOrDefault(),
54+
},
55+
)
56+
57+
// Create a whisper client
58+
opts := []client.ClientOpt{}
59+
if cli.Globals.Debug {
60+
opts = append(opts, client.OptTrace(os.Stderr, true))
61+
}
62+
client, err := api.New(cli.Globals.Endpoint, opts...)
63+
if err != nil {
64+
cmd.FatalIfErrorf(err)
65+
} else {
66+
cli.Globals.api = client
67+
}
68+
69+
// Create a tablewriter object with text output
70+
writer := tablewriter.New(os.Stdout, tablewriter.OptOutputText())
71+
cli.Globals.writer = writer
72+
73+
// Create a context
74+
cli.Globals.ctx = ctx.ContextForSignal(os.Interrupt, syscall.SIGQUIT)
75+
76+
// Run the command
77+
if err := cmd.Run(&cli.Globals); err != nil {
78+
cmd.FatalIfErrorf(err)
79+
}
80+
}
81+
82+
func endpointEnvOrDefault() string {
83+
if endpoint := os.Getenv("WHISPER_URL"); endpoint != "" {
84+
return endpoint
85+
}
86+
return "http://localhost:8080/v1"
87+
}

cmd/cli/models.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import tablewriter "github.com/djthorpe/go-tablewriter"
4+
5+
type ModelsCmd struct{}
6+
7+
func (_ *ModelsCmd) Run(ctx *Globals) error {
8+
models, err := ctx.api.ListModels(ctx.ctx)
9+
if err != nil {
10+
return err
11+
}
12+
return ctx.writer.Write(models, tablewriter.OptHeader())
13+
}

cmd/cli/transcribe.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/djthorpe/go-tablewriter"
7+
"github.com/mutablelogic/go-whisper/pkg/client"
8+
)
9+
10+
type TranscribeCmd struct {
11+
Model string `arg:"" required:"" help:"Model Identifier" type:"string"`
12+
Path string `arg:"" required:"" help:"Audio File Path" type:"string"`
13+
Language string `flag:"language" help:"Source Language" type:"string"`
14+
Prompt string `flag:"prompt" help:"Initial Prompt Identifier" type:"string"`
15+
Temperature *float32 `flag:"temperature" help:"Temperature" type:"float32"`
16+
}
17+
18+
func (cmd *TranscribeCmd) Run(ctx *Globals) error {
19+
r, err := os.Open(cmd.Path)
20+
if err != nil {
21+
return err
22+
}
23+
defer r.Close()
24+
25+
opts := []client.Opt{}
26+
if cmd.Language != "" {
27+
opts = append(opts, client.OptLanguage(cmd.Language))
28+
}
29+
if cmd.Prompt != "" {
30+
opts = append(opts, client.OptPrompt(cmd.Prompt))
31+
}
32+
if cmd.Temperature != nil {
33+
opts = append(opts, client.OptTemperature(*cmd.Temperature))
34+
}
35+
36+
transcription, err := ctx.api.Transcribe(ctx.ctx, cmd.Model, r, opts...)
37+
if err != nil {
38+
return err
39+
}
40+
return ctx.writer.Write(transcription, tablewriter.OptHeader())
41+
}

cmd/cli/translate.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/djthorpe/go-tablewriter"
7+
"github.com/mutablelogic/go-whisper/pkg/client"
8+
)
9+
10+
type TranslateCmd struct {
11+
Model string `arg:"" required:"" help:"Model Identifier" type:"string"`
12+
Path string `arg:"" required:"" help:"Audio File Path" type:"string"`
13+
Language string `flag:"language" required:"" help:"Target Language" type:"string"`
14+
Prompt string `flag:"prompt" help:"Initial Prompt Identifier" type:"string"`
15+
Temperature *float32 `flag:"temperature" help:"Temperature" type:"float32"`
16+
}
17+
18+
func (cmd *TranslateCmd) Run(ctx *Globals) error {
19+
r, err := os.Open(cmd.Path)
20+
if err != nil {
21+
return err
22+
}
23+
defer r.Close()
24+
25+
opts := []client.Opt{}
26+
if cmd.Language != "" {
27+
opts = append(opts, client.OptLanguage(cmd.Language))
28+
}
29+
if cmd.Prompt != "" {
30+
opts = append(opts, client.OptPrompt(cmd.Prompt))
31+
}
32+
if cmd.Temperature != nil {
33+
opts = append(opts, client.OptTemperature(*cmd.Temperature))
34+
}
35+
36+
transcription, err := ctx.api.Translate(ctx.ctx, cmd.Model, r, opts...)
37+
if err != nil {
38+
return err
39+
}
40+
return ctx.writer.Write(transcription, tablewriter.OptHeader())
41+
}

cmd/server/main.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import (
66
"net/http"
77
"os"
88
"path/filepath"
9+
"strconv"
910
"strings"
11+
"syscall"
1012

1113
// Packages
14+
context "github.com/mutablelogic/go-server/pkg/context"
15+
httpserver "github.com/mutablelogic/go-server/pkg/httpserver"
1216
api "github.com/mutablelogic/go-whisper/pkg/api"
1317
whisper "github.com/mutablelogic/go-whisper/pkg/whisper"
1418
sys "github.com/mutablelogic/go-whisper/sys/whisper"
@@ -27,7 +31,7 @@ func main() {
2731

2832
// Set logging
2933
sys.Whisper_log_set(func(level sys.LogLevel, text string) {
30-
if flags.Debug() && level == sys.LogLevelDebug || level == sys.LogLevelInfo || level == sys.LogLevelWarn {
34+
if flags.Debug() && (level == sys.LogLevelDebug || level == sys.LogLevelInfo || level == sys.LogLevelWarn) {
3135
return
3236
}
3337
log.Println(level, strings.TrimSpace(text))
@@ -58,13 +62,44 @@ func main() {
5862
os.Exit(-2)
5963
}
6064

61-
// Register the endpoints
62-
api.RegisterEndpoints(flags.Endpoint(), http.DefaultServeMux, whisper)
65+
// Display models
66+
var models []string
67+
for _, model := range whisper.ListModels() {
68+
models = append(models, strconv.Quote(model.Id))
69+
}
70+
if len(models) > 0 {
71+
log.Println("Models:", strings.Join(models, ", "))
72+
} else {
73+
log.Println("No models")
74+
}
75+
76+
// Create a mux for serving requests, then register the endpoints with the mux
77+
mux := http.NewServeMux()
78+
api.RegisterEndpoints(flags.Endpoint(), mux, whisper)
6379

64-
// Start the server
65-
log.Println("Listening on", flags.Listen())
66-
if err := http.ListenAndServe(flags.Listen(), nil); err != nil {
80+
// Create a new HTTP server
81+
log.Println("List address", flags.Listen())
82+
server, err := httpserver.Config{
83+
Listen: flags.Listen(),
84+
Router: mux,
85+
}.New()
86+
if err != nil {
6787
log.Println(err)
6888
os.Exit(-2)
6989
}
90+
91+
// Run the server until CTRL+C
92+
log.Println("Press CTRL+C to exit")
93+
ctx := context.ContextForSignal(os.Interrupt, syscall.SIGQUIT)
94+
if err := server.Run(ctx); err != nil {
95+
log.Println(err)
96+
os.Exit(-3)
97+
}
98+
99+
// Release whisper resources
100+
log.Println("Terminating")
101+
if err := whisper.Close(); err != nil {
102+
log.Println(err)
103+
os.Exit(-4)
104+
}
70105
}

go.mod

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ module github.com/mutablelogic/go-whisper
33
go 1.22
44

55
require (
6+
github.com/alecthomas/kong v0.9.0
7+
github.com/djthorpe/go-tablewriter v0.0.7
68
github.com/go-audio/wav v1.1.0
9+
github.com/mutablelogic/go-client v1.0.8
10+
github.com/mutablelogic/go-server v1.4.10
711
github.com/stretchr/testify v1.9.0
8-
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
912
)
1013

1114
require (
1215
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/djthorpe/go-errors v1.0.3 // indirect
1317
github.com/go-audio/audio v1.0.0 // indirect
1418
github.com/go-audio/riff v1.0.0 // indirect
19+
github.com/mattn/go-runewidth v0.0.15 // indirect
1520
github.com/pmezard/go-difflib v1.0.0 // indirect
21+
github.com/rivo/uniseg v0.4.7 // indirect
22+
golang.org/x/sys v0.21.0 // indirect
23+
golang.org/x/term v0.21.0 // indirect
1624
gopkg.in/yaml.v3 v3.0.1 // indirect
1725
)

0 commit comments

Comments
 (0)