Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
module github.com/richiejp/VoxInput

go 1.24.2
go 1.25

require (
fyne.io/fyne/v2 v2.7.1
github.com/WqyJh/go-openai-realtime v0.6.1
github.com/gen2brain/malgo v0.11.24
github.com/k2-fsa/sherpa-onnx-go-linux v1.12.19
github.com/sashabaranov/go-openai v1.41.2
github.com/spf13/pflag v1.0.10
github.com/tphakala/go-audio-resampler v1.1.0
)

replace github.com/k2-fsa/sherpa-onnx-go-linux v1.12.19 => ./sherpa-onnx-go-linux

require (
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
Expand All @@ -29,6 +34,9 @@ require (
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/k2-fsa/sherpa-onnx-go-linux v1.12.19 // indirect
github.com/k2-fsa/sherpa-onnx-go-macos v1.12.19 // indirect
github.com/k2-fsa/sherpa-onnx-go-windows v1.12.19 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
Expand All @@ -37,10 +45,12 @@ require (
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tphakala/simd v1.0.14 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.23.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wH
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/k2-fsa/sherpa-onnx-go v1.12.19 h1:sWY5/pV8njH59/L9pFPS0L1z5l7Tczrjo0k73N4AYas=
github.com/k2-fsa/sherpa-onnx-go v1.12.19/go.mod h1:B/ynRbVa5gpYoZYeYgY3zPi4MTfKk95UZueZDSIhbjk=
github.com/k2-fsa/sherpa-onnx-go-linux v1.12.19 h1:k3+yT9KxZSXOVBh0Bv2HCGi2Cv8QTNY56ik/9P/W6oQ=
github.com/k2-fsa/sherpa-onnx-go-linux v1.12.19/go.mod h1:NXEH2rsBgTdqY59YpPq6CtSBlBAXy/8a9FmpLERU97I=
github.com/k2-fsa/sherpa-onnx-go-macos v1.12.19 h1:+ELgK6N/LHbW5AqD3tshPcEj3X0BDPwyhf5ehVPZo74=
github.com/k2-fsa/sherpa-onnx-go-macos v1.12.19/go.mod h1:ZOhUAXC62Unj0ZNfu6zxSFKcW96aXf7P3BsqiUyOBbE=
github.com/k2-fsa/sherpa-onnx-go-windows v1.12.19 h1:qYG2c7p/pPLTpj1JuOd/UsewMB87LbS+67YG6fGsBDQ=
github.com/k2-fsa/sherpa-onnx-go-windows v1.12.19/go.mod h1:5AX7TU8+P/gInjglY1ijtWUM2b8iyR0QX4yEngzMe64=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
Expand All @@ -67,22 +75,36 @@ github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tphakala/go-audio-resampler v1.1.0 h1:3r9ny8msAYBA6Y05oDzrPRCTLFdGDZHwMcUpyIABU08=
github.com/tphakala/go-audio-resampler v1.1.0/go.mod h1:bO2D6Qb7niR+14RAl076Zu+3QnkDcZg+UdLGoSGi5hI=
github.com/tphakala/simd v1.0.14 h1:FisR7bAdTVzZeY7cqMfqIEoAtWeRoBXl9XXtCbD3fc0=
github.com/tphakala/simd v1.0.14/go.mod h1:8xsPUbOTnNI4WUdPlXVlWXt85Y8RCm3xqGAo8PLxYyA=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
145 changes: 49 additions & 96 deletions internal/gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ package gui

import (
"context"
"fmt"
"time"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)

type Msg interface {
Expand All @@ -33,34 +28,56 @@ func (m *HideMsg) IsMsg() bool { return true }
func (m *ShowStoppingMsg) IsMsg() bool { return true }

type GUI struct {
a fyne.App
w fyne.Window
Chan chan Msg
cancelTimer context.CancelFunc
timerCtx context.Context
statusLabel *widget.Label
statusIcon *widget.Icon
countDown *widget.ProgressBar
a fyne.App
Chan chan Msg
ListenIcon fyne.Resource
DetectedIcon fyne.Resource
TranscribingIcon fyne.Resource
StopIcon fyne.Resource
}

// TODO:: The App Icon does not work in the sys tray: https://github.com/fyne-io/fyne/issues/3968
func makeTray(a fyne.App) {
if desk, ok := a.(desktop.App); ok {
func makeTray(ui GUI) {
if desk, ok := ui.a.(desktop.App); ok {
menu := fyne.NewMenu("VoxInput")
desk.SetSystemTrayMenu(menu)
desk.SetSystemTrayIcon(ui.TranscribingIcon)
}
}

func New(ctx context.Context, showStatus string) *GUI {
a := app.NewWithID("voxinput")
a.SetIcon(theme.MediaRecordIcon())

ui := GUI{
a: a,
Chan: make(chan Msg),
}

makeTray(a)
// Load icons
if ListenIcon, err := fyne.LoadResourceFromPath("icons/microphone.png"); err != nil {
panic(err)
} else {
ui.ListenIcon = ListenIcon
}
if DetectedIcon, err := fyne.LoadResourceFromPath("icons/play.png"); err != nil {
panic(err)
} else {
ui.DetectedIcon = DetectedIcon
}
if TranscribingIcon, err := fyne.LoadResourceFromPath("icons/voice-note.png"); err != nil {
panic(err)
} else {
ui.TranscribingIcon = TranscribingIcon
}
if StopIcon, err := fyne.LoadResourceFromPath("icons/pause.png"); err != nil {
panic(err)
} else {
ui.StopIcon = StopIcon
}

a.SetIcon(ui.TranscribingIcon)

makeTray(ui)

go func() {
for {
Expand All @@ -70,30 +87,23 @@ func New(ctx context.Context, showStatus string) *GUI {
switch msg.(type) {
case *ShowListeningMsg:
if showStatus != "" {
ui.showStatus("Listening with voice audio detection...", theme.MediaRecordIcon())
ui.showStatus("Listening with voice audio detection...", ui.ListenIcon)
}
case *ShowSpeechDetectedMsg:
if showStatus != "" {
ui.showStatus("Detected speech...", theme.MediaMusicIcon())
ui.showStatus("Detected speech...", ui.DetectedIcon)
}
case *ShowTranscribingMsg:
if showStatus != "" {
ui.showStatus("Transcribing...", theme.FileTextIcon())
ui.showStatus("Transcribing...", ui.TranscribingIcon)
}
case *ShowGeneratingResponseMsg:
if showStatus != "" {
ui.showStatus("Generating response...", theme.FileAudioIcon())
}
case *HideMsg:
if ui.cancelTimer != nil {
ui.cancelTimer()
}
if ui.w != nil {
ui.w.Hide()
ui.showStatus("Generating response...", ui.DetectedIcon)
}
case *ShowStoppingMsg:
if showStatus != "" {
ui.showStatus("Stopping listening", theme.MediaStopIcon())
ui.showStatus("Stopping listening", ui.StopIcon)
}
}
})
Expand All @@ -114,72 +124,15 @@ func (g *GUI) Run() {
}

func (g *GUI) showStatus(statusText string, icon fyne.Resource) {
if g.cancelTimer != nil {
g.cancelTimer()
}

if g.w == nil {
g.w = g.a.NewWindow("VoxInput")
g.w.SetFixedSize(true)
g.w.Resize(fyne.NewSize(300, 150))

g.statusLabel = widget.NewLabel(statusText)
g.statusIcon = widget.NewIcon(icon)
g.statusIcon.Resize(fyne.NewSize(100, 100))

g.countDown = widget.NewProgressBar()
g.countDown.TextFormatter = func() string {
return ""
}

g.w.SetContent(container.NewGridWithColumns(1,
g.statusLabel,
g.statusIcon,
g.countDown,
))
} else {
g.statusLabel.SetText(statusText)
g.statusIcon.SetResource(icon)
if desk, ok := g.a.(desktop.App); ok {
m := fyne.NewMenu("VoxInput",
fyne.NewMenuItem(statusText, nil),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("Quit", func() {
g.a.Quit()
}),
)
desk.SetSystemTrayIcon(icon)
desk.SetSystemTrayMenu(m)
}

var ticks time.Duration
tickTime := time.Millisecond * 50
closeTimeout := 1500 * time.Millisecond

g.countDown.TextFormatter = func() string {
return fmt.Sprintf("Closing in %.2fs", closeTimeout.Seconds()-ticks.Seconds())
}
g.countDown.SetValue(0)
g.countDown.Refresh()

g.timerCtx, g.cancelTimer = context.WithCancel(context.Background())
timerCtx := g.timerCtx

go func() {
ticker := time.NewTicker(tickTime)
defer ticker.Stop()

for {
select {
case <-timerCtx.Done():
return
case <-ticker.C:
ticks += tickTime

fyne.Do(func() {
g.countDown.SetValue(float64(ticks) / float64(closeTimeout))

if ticks > closeTimeout {
g.w.Hide()
}
})

if ticks > closeTimeout {
return
}
}
}
}()

g.w.Show()
}
Loading