diff --git a/go/go-speech-transcriber/README.md b/go/go-speech-transcriber/README.md new file mode 100644 index 0000000..75f697c --- /dev/null +++ b/go/go-speech-transcriber/README.md @@ -0,0 +1,163 @@ +# Go Speech Transcriber + +A desktop application that transcribes speech to text in real-time and types it into your active window. Perfect for dictating emails, messages, or documents hands-free. + +## Features + +- **Real-time speech-to-text** using Gladia's API +- **Multi-language support** with language selection in system tray +- **System tray controls** for easy access +- **Keyboard shortcuts** to start/stop recording +- **Automatic typing** of transcribed text into the active application + +## Installation + +### Prerequisites + +- Go 1.18 or higher +- Audio input device +- Gladia API key + +### Install from source + +```bash +# Clone the repository +git clone https://github.com/gladiaio/go-speech-transcriber +cd go-speech-transcriber + +# Install dependencies +go mod download + +# Build the application +go build -o speech-transcriber +``` + +### Compilation Guide + +#### Build for Different Platforms + +**Windows:** +```bash +GOOS=windows GOARCH=amd64 go build -o speech-transcriber.exe +``` + +**macOS:** +```bash +GOOS=darwin GOARCH=amd64 go build -o speech-transcriber-amd64 +GOOS=darwin GOARCH=arm64 go build -o speech-transcriber-arm64 + +# Create a universal binary (for both Intel and M1/M2 Macs) +lipo -create -output speech-transcriber speech-transcriber-amd64 speech-transcriber-arm64 +``` + +**Linux:** +```bash +GOOS=linux GOARCH=amd64 go build -o speech-transcriber +``` + +#### Release Builds + +For optimized release builds with smaller binary size: + +```bash +go build -ldflags="-s -w" -o speech-transcriber +``` + +## Configuration + +You can provide your Gladia API key in a `.env` file: + +``` +GLADIA_API_KEY=your_gladia_api_key +``` + +Or pass it as a command-line argument (see Usage section). + +## Key Components + +- **SpeechTranscriber**: Handles transcription and keyboard input +- **GladiaRecorder**: Manages audio recording and communication with Gladia API +- **StatusBarApp**: Implements system tray functionality +- **KeyListener**: Handles keyboard shortcuts + +## Usage + +### Basic usage + +```bash +./speech-transcriber +``` + +### Command line options + +```bash +# Use custom key combination (default: cmd_l+alt) +./speech-transcriber -key=ctrl+shift+r + +# Use double right Command key press to toggle recording +./speech-transcriber -double_cmd + +# Set maximum recording time (default: 30 seconds) +./speech-transcriber -max_time=60 + +# Specify supported languages +./speech-transcriber -language=en -language=fr + +# Provide Gladia API key via command line +./speech-transcriber -gladia_api_key=your_api_key +``` + +## API Keys + +- **Gladia API**: Required for speech transcription. Get your key at [https://gladia.io](https://gladia.io) + +## Keyboard Controls + +- Press the configured key combination (default: Left Command+Option on macOS) to start/stop recording +- If using double_cmd option, press Right Command key twice quickly to toggle recording +- Alternatively, use the system tray menu to control recording + +## How It Works + +1. Start recording using the keyboard shortcut or system tray +2. Speak clearly into your microphone +3. Stop recording using the same shortcut or system tray +4. The transcribed text will automatically be typed into your active window + +## Dependencies + +The application uses these key libraries: +- github.com/getlantern/systray - System tray functionality +- github.com/gordonklaus/portaudio - Audio capture +- github.com/gorilla/websocket - WebSocket communication with Gladia API +- github.com/micmonay/keybd_event - Keyboard simulation +- github.com/robotn/gohook - Global keyboard hooks + +## Troubleshooting + +### MacOS Build Warnings + +When building on macOS, you might see warnings about duplicate libraries. These can be safely ignored. + +### Windows Build Issues + +On Windows, you might need to install MinGW-w64 for GCC and ensure it's in your PATH. + +### Linux Build Dependencies + +On Linux, install these dependencies first: + +```bash +# Ubuntu/Debian +sudo apt-get install libx11-dev xorg-dev libxtst-dev libpng++-dev libasound2-dev + +# Fedora/CentOS +sudo dnf install libX11-devel xorg-x11-devel libXtst-devel libpng-devel alsa-lib-devel +``` + +### Keyboard Shortcut Issues + +If the default keyboard shortcut doesn't work: +1. Try using the `-key` flag to set a different key combination +2. Use the `-double_cmd` flag to use double Right Command key press instead +3. Check the system tray menu to manually start/stop recording diff --git a/go/go-speech-transcriber/go-speech-transcriber.go b/go/go-speech-transcriber/go-speech-transcriber.go new file mode 100644 index 0000000..442f951 --- /dev/null +++ b/go/go-speech-transcriber/go-speech-transcriber.go @@ -0,0 +1,821 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "time" + + "github.com/getlantern/systray" + "github.com/gordonklaus/portaudio" + "github.com/gorilla/websocket" + "github.com/micmonay/keybd_event" + hook "github.com/robotn/gohook" +) + +const ( + GLADIA_API_URL = "https://api.gladia.io" +) + +// Config represents the application configuration +type Config struct { + KeyCombination string + DoubleCmd bool + Languages []string + MaxTime float64 + GladiaAPIKey string +} + +// AudioTranscriptionService handles communication with the Gladia API +type AudioTranscriptionService struct { + APIKey string +} + +// Initialize a session with the Gladia API +func (s *AudioTranscriptionService) InitializeSession(language string) (map[string]interface{}, error) { + + config := map[string]interface{}{ + "encoding": "wav/pcm", + "sample_rate": 16000, + "bit_depth": 16, + "channels": 1, + "language_config": map[string]interface{}{ + "languages": []string{}, + "code_switching": true, + }, + } + + if language != "" { + config["language_config"].(map[string]interface{})["languages"] = []string{language} + } + + jsonData, err := json.Marshal(config) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", GLADIA_API_URL+"/v2/live", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-gladia-key", s.APIKey) + + log.Printf("Sending request to Gladia API: %s", GLADIA_API_URL+"/v2/live") + + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Accept both 200 OK and 201 Created as successful responses + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("error initializing Gladia API: %d: %s", resp.StatusCode, string(body)) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + log.Printf("Successfully initialized Gladia API session with status code: %d", resp.StatusCode) + return result, nil +} + +// SpeechTranscriber handles the main transcription logic +type SpeechTranscriber struct { + Keyboard *keybd_event.KeyBonding +} + +// GladiaRecorder handles audio recording and communication with Gladia API +type GladiaRecorder struct { + Transcriber *SpeechTranscriber + Recording bool + APIKey string + CurrentText string + WebSocket *websocket.Conn + TranscriptChan chan string + Done chan bool + Mutex sync.Mutex +} + +func NewGladiaRecorder(transcriber *SpeechTranscriber, apiKey string) *GladiaRecorder { + if apiKey == "" { + apiKey = os.Getenv("GLADIA_API_KEY") + } + + return &GladiaRecorder{ + Transcriber: transcriber, + Recording: false, + APIKey: apiKey, + CurrentText: "", + TranscriptChan: make(chan string), + Done: make(chan bool), + } +} + +func (r *GladiaRecorder) Start(language string) error { + r.Mutex.Lock() + if r.Recording { + r.Mutex.Unlock() + return fmt.Errorf("recording already in progress") + } + r.Recording = true + r.Mutex.Unlock() + + // Initialize audio session and start recording in a goroutine + go r.startRecording(language) + return nil +} + +func (r *GladiaRecorder) Stop() { + r.Mutex.Lock() + if !r.Recording { + r.Mutex.Unlock() + return + } + r.Recording = false + r.Mutex.Unlock() + + // Signal recording to stop + r.Done <- true + + // Add a small delay to ensure the typeText method has time to start + time.Sleep(100 * time.Millisecond) +} + +func (r *GladiaRecorder) startRecording(language string) { + log.Printf("Starting recording with Gladia API for language: %s", language) + + audioService := AudioTranscriptionService{APIKey: r.APIKey} + sessionInfo, err := audioService.InitializeSession(language) + if err != nil { + log.Printf("Error initializing Gladia session: %v", err) + return + } + + wsURL, ok := sessionInfo["url"].(string) + if !ok { + log.Printf("Missing WebSocket URL in session info. Response: %v", sessionInfo) + return + } + + log.Printf("Got WebSocket URL: %s", wsURL) + + // Connect to WebSocket + log.Printf("Connecting to Gladia WebSocket...") + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + log.Printf("WebSocket connection error: %v", err) + if resp != nil { + log.Printf("WebSocket response status: %s", resp.Status) + } + return + } + log.Printf("Successfully connected to Gladia WebSocket") + + r.WebSocket = conn + defer conn.Close() + + // Start audio processing and transcription handling + var wg sync.WaitGroup + wg.Add(2) + + // Process incoming transcriptions + go func() { + defer wg.Done() + for { + _, message, err := conn.ReadMessage() + if err != nil { + if r.Recording { + log.Printf("WebSocket read error: %v", err) + } + return + } + + var content map[string]interface{} + if err := json.Unmarshal(message, &content); err != nil { + log.Printf("Error parsing message: %v", err) + continue + } + + // Handle transcript messages + if contentType, ok := content["type"].(string); ok && contentType == "transcript" { + data, ok := content["data"].(map[string]interface{}) + if !ok { + continue + } + + isFinal, ok := data["is_final"].(bool) + if !ok || !isFinal { + continue + } + + utterance, ok := data["utterance"].(map[string]interface{}) + if !ok { + continue + } + + text, ok := utterance["text"].(string) + if !ok { + continue + } + + text = strings.TrimSpace(text) + + r.Mutex.Lock() + if r.CurrentText != "" { + r.CurrentText += " " + text + } else { + r.CurrentText = text + } + log.Printf("Current transcription: %s", r.CurrentText) + r.Mutex.Unlock() + } + } + }() + + // Send audio data + go func() { + defer wg.Done() + + // Initialize PortAudio + if err := portaudio.Initialize(); err != nil { + log.Printf("Error initializing audio: %v", err) + return + } + defer portaudio.Terminate() + + // Set up audio parameters + framesPerBuffer := 3200 + sampleRate := 16000 + channels := 1 + + // Buffer for audio data + buffer := make([]int16, framesPerBuffer) + + // Create audio input stream - Pass the buffer here instead of nil + stream, err := portaudio.OpenDefaultStream(channels, 0, float64(sampleRate), framesPerBuffer, buffer) + if err != nil { + log.Printf("Error opening audio stream: %v", err) + return + } + defer stream.Close() + + if err := stream.Start(); err != nil { + log.Printf("Error starting audio stream: %v", err) + return + } + defer stream.Stop() + + // Send audio data until recording is stopped + for r.Recording { + if err := stream.Read(); err != nil { + log.Printf("Error reading from audio stream: %v", err) + return + } + + // Convert buffer to bytes - FIX THE BYTE CONVERSION + audioBytes := make([]byte, len(buffer)*2) + for i, sample := range buffer { + // Use little-endian byte order (LSB first) + audioBytes[i*2] = byte(sample & 0xFF) // Low byte + audioBytes[i*2+1] = byte((sample >> 8) & 0xFF) // High byte + } + + encodedData := base64.StdEncoding.EncodeToString(audioBytes) + + // Create message with audio chunk + message := map[string]interface{}{ + "type": "audio_chunk", + "data": map[string]interface{}{ + "chunk": encodedData, + }, + } + + messageJSON, err := json.Marshal(message) + if err != nil { + log.Printf("Error encoding audio message: %v", err) + continue + } + + if err := conn.WriteMessage(websocket.TextMessage, messageJSON); err != nil { + log.Printf("Error sending audio chunk: %v", err) + return + } + + time.Sleep(100 * time.Millisecond) + } + + // Send stop recording message + stopMsg := map[string]interface{}{ + "type": "stop_recording", + } + stopMsgJSON, _ := json.Marshal(stopMsg) + conn.WriteMessage(websocket.TextMessage, stopMsgJSON) + + // Type the final text after processing + go r.typeText() + }() + + // Wait for done signal or completion + select { + case <-r.Done: + r.Recording = false + } + + wg.Wait() +} + +func (r *GladiaRecorder) typeText() { + // Add a small delay to ensure the recording has fully stopped + time.Sleep(500 * time.Millisecond) + + r.Mutex.Lock() + text := r.CurrentText + r.CurrentText = "" // Reset for next recording + r.Mutex.Unlock() + + if text == "" { + log.Println("No text to type") + return + } + + log.Printf("Typing text: %s", text) + + // Try using clipboard instead of direct typing + // This is more reliable for longer text + if err := copyToClipboard(text); err != nil { + log.Printf("Error copying to clipboard: %v", err) + // Fall back to character-by-character typing + typeCharByChar(text) + } else { + // Paste from clipboard (Cmd+V) + kb, err := keybd_event.NewKeyBonding() + if err != nil { + log.Printf("Error creating keyboard controller: %v", err) + return + } + + // Press Cmd+V to paste + kb.SetKeys(keybd_event.VK_V) + kb.HasCTRL(runtime.GOOS == "windows") // Use Ctrl on Windows + // Use HasSuper instead of HasMETA for Command key on macOS + kb.HasSuper(runtime.GOOS == "darwin") // Use Cmd on macOS + + if err := kb.Launching(); err != nil { + log.Printf("Error pasting from clipboard: %v", err) + // Fall back to character-by-character typing + typeCharByChar(text) + } + } + + log.Println("Finished typing text") +} + +// Helper function to copy text to clipboard +func copyToClipboard(text string) error { + // Use exec.Command to run pbcopy on macOS or clip on Windows + var cmd *exec.Cmd + + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("pbcopy") + case "windows": + cmd = exec.Command("clip") + default: + return fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } + + cmd.Stdin = strings.NewReader(text) + return cmd.Run() +} + +// Helper function to type character by character with improved reliability +func typeCharByChar(text string) { + // Create a new keyboard controller + kb, err := keybd_event.NewKeyBonding() + if err != nil { + log.Printf("Error creating keyboard controller: %v", err) + return + } + + // Type each character with a longer delay + for _, char := range text { + // Reset the keyboard state for each character + kb.Clear() + kb.SetKeys(int(char)) + + // Try launching with retry + for i := 0; i < 3; i++ { + if err := kb.Launching(); err != nil { + log.Printf("Error typing character (attempt %d): %v", i+1, err) + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + + // Add a longer delay between characters + time.Sleep(20 * time.Millisecond) + } +} + +// StatusBarApp represents the system tray application +type StatusBarApp struct { + Recorder *GladiaRecorder + Languages []string + CurrentLanguage string + MaxTime float64 + Started bool + Timer *time.Timer + StartTime time.Time +} + +func NewStatusBarApp(recorder *GladiaRecorder, languages []string, maxTime float64) *StatusBarApp { + currentLang := "" + if len(languages) > 0 { + currentLang = languages[0] + } + + return &StatusBarApp{ + Recorder: recorder, + Languages: languages, + CurrentLanguage: currentLang, + MaxTime: maxTime, + Started: false, + } +} + +func (app *StatusBarApp) Run() { + systray.Run(app.onReady, app.onExit) +} + +func (app *StatusBarApp) onReady() { + systray.SetTitle("Whisper") + systray.SetTooltip("Speech Transcriber") + + mStartRecord := systray.AddMenuItem("Start Recording", "Start recording and transcribing") + mStopRecord := systray.AddMenuItem("Stop Recording", "Stop recording and transcribing") + mStopRecord.Disable() + + systray.AddSeparator() + + // Add language menu items if provided + langMenuItems := make(map[string]*systray.MenuItem) + if len(app.Languages) > 0 { + for _, lang := range app.Languages { + langMenuItem := systray.AddMenuItem(lang, "Switch to "+lang) + langMenuItems[lang] = langMenuItem + + if lang == app.CurrentLanguage { + langMenuItem.Disable() + } + } + } + + systray.AddSeparator() + mQuit := systray.AddMenuItem("Quit", "Quit the application") + + // Handle menu events in a goroutine + go func() { + for { + select { + case <-mStartRecord.ClickedCh: + if !app.Started { + app.Start() + mStartRecord.Disable() + mStopRecord.Enable() + } + + case <-mStopRecord.ClickedCh: + if app.Started { + app.Stop() + mStopRecord.Disable() + mStartRecord.Enable() + } + + case <-mQuit.ClickedCh: + if app.Started { + app.Stop() + } + systray.Quit() + return + + default: + // Check if any language menu item was clicked + for lang, menuItem := range langMenuItems { + select { + case <-menuItem.ClickedCh: + app.CurrentLanguage = lang + + // Update enabled/disabled state + for l, mi := range langMenuItems { + if l == lang { + mi.Disable() + } else { + mi.Enable() + } + } + + default: + // No click for this language + } + } + + time.Sleep(100 * time.Millisecond) + } + } + }() +} + +func (app *StatusBarApp) onExit() { + // Cleanup +} + +func (app *StatusBarApp) Start() { + log.Println("Listening with Gladia API...") + app.Started = true + systray.SetTitle("🔴 Recording") + + // Start recording + err := app.Recorder.Start(app.CurrentLanguage) + if err != nil { + log.Printf("Error starting recording: %v", err) + app.Stop() + return + } + + app.StartTime = time.Now() + + // Start the timer for max recording time if specified + if app.MaxTime > 0 { + app.Timer = time.AfterFunc(time.Duration(app.MaxTime*float64(time.Second)), func() { + app.Stop() + }) + } + + // Start the timer update goroutine + go app.updateTitle() +} + +func (app *StatusBarApp) Stop() { + if !app.Started { + return + } + + // Cancel the timer if it's running + if app.Timer != nil { + app.Timer.Stop() + } + + log.Println("Performing Gladia's transcription...") + systray.SetTitle("⏯") + app.Started = false + + // Stop recording + app.Recorder.Stop() +} + +func (app *StatusBarApp) updateTitle() { + for app.Started { + elapsed := time.Since(app.StartTime) + minutes := int(elapsed.Minutes()) + seconds := int(elapsed.Seconds()) % 60 + + systray.SetTitle(fmt.Sprintf("(%02d:%02d) 🔴", minutes, seconds)) + time.Sleep(1 * time.Second) + } +} + +func (app *StatusBarApp) Toggle() { + if app.Started { + app.Stop() + } else { + app.Start() + } +} + +// KeyListener handles global keyboard shortcuts +type KeyListener struct { + App *StatusBarApp + KeyCombination []int + KeysPressed map[int]bool + DoubleCmd bool + LastPressTime time.Time + LastReleasedKey int + LastReleaseTime time.Time +} + +func NewKeyListener(app *StatusBarApp, keyCombination string, doubleCmd bool) *KeyListener { + keys := parseKeyCombination(keyCombination) + + return &KeyListener{ + App: app, + KeyCombination: keys, + KeysPressed: make(map[int]bool), + DoubleCmd: doubleCmd, + LastPressTime: time.Time{}, + LastReleasedKey: 0, + LastReleaseTime: time.Time{}, + } +} + +func parseKeyCombination(combination string) []int { + parts := strings.Split(combination, "+") + keys := make([]int, 0, len(parts)) + + // Updated key codes based on your system's actual values + keyMap := map[string]int{ + "cmd_l": 56, // Left Command on your system (was 55) + "cmd_r": 54, // Right Command + "alt": 3675, // Option/Alt on your system (was 58) + "ctrl": 59, // Control + "shift": 56, // Shift + } + + for _, part := range parts { + if key, ok := keyMap[part]; ok { + keys = append(keys, key) + } else if len(part) == 1 { + keys = append(keys, int(part[0])) + } + } + + return keys +} + +func (l *KeyListener) Start() { + // Replace the generic message with a more helpful one + if l.DoubleCmd { + log.Println("Ready! Press Right Command key twice quickly to start/stop transcription") + } else { + // Create a user-friendly description of the key combination + // Specify that it's the Left Command key + keyDesc := "Left Command+Option" + + // Log the instruction + log.Println("Ready! Press " + keyDesc + " to start/stop transcription") + } + + // Use hook package for keyboard events + events := hook.Start() + defer hook.End() + + for ev := range events { + if ev.Kind == hook.KeyDown { + l.handleKeyDown(int(ev.Keycode)) + } else if ev.Kind == hook.KeyUp { + l.handleKeyUp(int(ev.Keycode)) + } + } +} + +func (l *KeyListener) handleKeyDown(keyCode int) { + // Remove or comment out this log message + // log.Printf("Key pressed: %d", keyCode) + + if l.DoubleCmd { + if keyCode == 54 { // Mac right command key code + now := time.Now() + if !l.App.Started && now.Sub(l.LastPressTime) < 500*time.Millisecond { + // Double press detected + l.App.Toggle() + } else if l.App.Started { + // Single press while recording + l.App.Toggle() + } + l.LastPressTime = now + } + } else { + // Handle key combination + l.KeysPressed[keyCode] = true + + // Remove or comment out these verbose log messages + // log.Printf("Currently pressed keys: %v", l.KeysPressed) + // log.Printf("Looking for combination: %v", l.KeyCombination) + + // Check specifically for cmd_l+alt combination using your system's key codes + if l.KeysPressed[56] && l.KeysPressed[3675] { + // Force toggle the recording when these specific keys are detected + l.App.Toggle() + } + + // Keep the original logic as a fallback + allPressed := true + for _, key := range l.KeyCombination { + if !l.KeysPressed[key] { + allPressed = false + break + } + } + + if allPressed { + l.App.Toggle() + } + } +} + +func (l *KeyListener) handleKeyUp(keyCode int) { + // Remove or comment out this log message + // log.Printf("Key released: %d", keyCode) + + // Check if we're releasing one of our target keys (56 or 3675) + if keyCode == 56 || keyCode == 3675 { + now := time.Now() + + // Check if this is the second key release in our sequence + if l.LastReleasedKey != 0 && l.LastReleasedKey != keyCode { + // Check if the previous key was released recently (within 500ms) + if now.Sub(l.LastReleaseTime) < 500*time.Millisecond { + // Check if we have the correct sequence (56 then 3675 OR 3675 then 56) + if (l.LastReleasedKey == 56 && keyCode == 3675) || + (l.LastReleasedKey == 3675 && keyCode == 56) { + l.App.Toggle() + } + } + } + + // Update the last released key info + l.LastReleasedKey = keyCode + l.LastReleaseTime = now + } + + // Remove the key from pressed keys map + delete(l.KeysPressed, keyCode) +} + +func main() { + // Parse command line arguments + keyCombination := flag.String("key", "cmd_l+alt", "Key combination to toggle recording") + doubleCmd := flag.Bool("double_cmd", false, "Use double Right Command key press to toggle") + maxTime := flag.Float64("max_time", 30.0, "Maximum recording time in seconds") + gladiaAPIKey := flag.String("gladia_api_key", "", "Gladia API key") + + // Language flags + var languages stringSliceFlag + flag.Var(&languages, "language", "Language code (can be specified multiple times)") + + flag.Parse() + + // Initialize portaudio + if err := portaudio.Initialize(); err != nil { + log.Fatalf("Error initializing PortAudio: %v", err) + } + defer portaudio.Terminate() + + // Create the keyboard controller + kb, err := keybd_event.NewKeyBonding() + if err != nil { + log.Fatalf("Error creating keyboard controller: %v", err) + } + + // Create transcriber (without rephrasing fields) + transcriber := &SpeechTranscriber{ + Keyboard: &kb, + } + + // Create recorder + apiKey := *gladiaAPIKey + if apiKey == "" { + apiKey = os.Getenv("GLADIA_API_KEY") + } + + recorder := NewGladiaRecorder(transcriber, apiKey) + + // Create status bar app + app := NewStatusBarApp(recorder, languages, *maxTime) + + // Create and start key listener in a goroutine + keyListener := NewKeyListener(app, *keyCombination, *doubleCmd) + go keyListener.Start() + + // Run the app + app.Run() +} + +// stringSliceFlag is a custom flag type for handling multiple string flags +type stringSliceFlag []string + +func (s *stringSliceFlag) String() string { + return strings.Join(*s, ", ") +} + +func (s *stringSliceFlag) Set(value string) error { + *s = append(*s, value) + return nil +} diff --git a/go/go-speech-transcriber/go.mod b/go/go-speech-transcriber/go.mod new file mode 100644 index 0000000..71d2dab --- /dev/null +++ b/go/go-speech-transcriber/go.mod @@ -0,0 +1,40 @@ +module github.com/gladiaio/go-speech-transcriber + +go 1.20 + +require ( + github.com/getlantern/systray v1.2.2 + github.com/go-vgo/robotgo v0.100.10 + github.com/gordonklaus/portaudio v0.0.0-20221027163845-7c3b689db3cc + github.com/gorilla/websocket v1.5.1 + github.com/micmonay/keybd_event v1.1.2 + github.com/robotn/gohook v0.40.0 +) + +require ( + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect + github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect + github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect + github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect + github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect + github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + github.com/otiai10/gosseract v2.2.1+incompatible // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 // indirect + github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 // indirect + github.com/shirou/gopsutil v3.21.10+incompatible // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/vcaesar/gops v0.21.3 // indirect + github.com/vcaesar/imgo v0.40.0 // indirect + github.com/vcaesar/keycode v0.10.0 // indirect + github.com/vcaesar/tt v0.20.1 // indirect + golang.org/x/image v0.15.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/go/go-speech-transcriber/go.sum b/go/go-speech-transcriber/go.sum new file mode 100644 index 0000000..f897a25 --- /dev/null +++ b/go/go-speech-transcriber/go.sum @@ -0,0 +1,79 @@ +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= +github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-vgo/robotgo v0.100.10 h1:bZe7AslG6oq5ops1SWUxsPfM9Z3QQvlqfA3ezxLFNO4= +github.com/go-vgo/robotgo v0.100.10/go.mod h1:7QeIpSHX7bjeXWRPxvQeKSx9mHI+3l80Ahq+CQF0C68= +github.com/gordonklaus/portaudio v0.0.0-20221027163845-7c3b689db3cc h1:yYLpN7bJxKYILKnk20oczGQOQd2h3/7z7/cxdD9Se/I= +github.com/gordonklaus/portaudio v0.0.0-20221027163845-7c3b689db3cc/go.mod h1:WY8R6YKlI2ZI3UyzFk7P6yGSuS+hFwNtEzrexRyD7Es= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/micmonay/keybd_event v1.1.2 h1:RpgvPJKOh4Jc+ZYe0OrVzGd2eNMCfuVg3dFTCsuSah4= +github.com/micmonay/keybd_event v1.1.2/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI= +github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I= +github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE= +github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robotn/gohook v0.40.0 h1:qqjyRUIoRwwa9yv4xVeL8hX+vdhc9j56p9kF0D+hUuM= +github.com/robotn/gohook v0.40.0/go.mod h1:wyGik0yb4iwCfJjDprtNkTyxkgQWuKoVPQ3hkz6+6js= +github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY= +github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ= +github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk= +github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU= +github.com/shirou/gopsutil v3.21.10+incompatible h1:AL2kpVykjkqeN+MFe1WcwSBVUjGjvdU8/ubvCuXAjrU= +github.com/shirou/gopsutil v3.21.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/vcaesar/gops v0.21.3 h1:VR7amkxVv9CQfsotkXrmMyT19dVuNTa1PM/oopJeIc0= +github.com/vcaesar/gops v0.21.3/go.mod h1:3e2EnlZTI9/44bqzRwkeZ3s0ZQwK2Cn4QPLx8Ii8Agk= +github.com/vcaesar/imgo v0.40.0 h1:okI1eonRAfGLzjqgTIBkUwhm4j/rH19qGno4eFOBQsc= +github.com/vcaesar/imgo v0.40.0/go.mod h1:E5uI53XkEfbI20VvcIZ/19G2hHidPfH9h4NtQooEY+8= +github.com/vcaesar/keycode v0.10.0 h1:Qx5QE8ZXHyRyjoA2QOxBp25OKMKB+zxMVqm0FWGV0d4= +github.com/vcaesar/keycode v0.10.0/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ= +github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4= +github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/go-speech-transcriber/speech-transcriber b/go/go-speech-transcriber/speech-transcriber new file mode 100755 index 0000000..57277bb Binary files /dev/null and b/go/go-speech-transcriber/speech-transcriber differ