acme-misc/speak/main.go

156 lines
3.1 KiB
Go

package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"syscall"
"9fans.net/go/acme"
)
func readLines(r io.Reader, ch chan string) {
buf := make([]byte, 1)
line := ""
for {
n, err := r.Read(buf)
if n > 0 {
if buf[0] == '\n' {
// Newline = finalized output, send it
if line != "" {
ch <- line
line = ""
}
} else if buf[0] == '\r' {
// Carriage return = overwrite, keep accumulating
line = ""
} else {
line += string(buf[0])
}
}
if err != nil {
if line != "" {
ch <- line
}
break
}
}
}
const (
stateFile = "/tmp/acme-voice-state"
modelPath = "/home/lkn/src/acme-speak/models/ggml-tiny.bin"
)
func main() {
// Kill any leftover whisper-stream processes
exec.Command("pkill", "-f", "whisper-stream").Run()
winid := os.Getenv("winid")
// Check if already running
if pidBytes, err := os.ReadFile(stateFile); err == nil {
// Stop existing stream - use syscall to kill process group
pid := strings.TrimSpace(string(pidBytes))
pidInt, _ := strconv.Atoi(pid)
if pidInt > 0 {
// Kill the process and its children
exec.Command("pkill", "-9", "-P", pid).Run()
syscall.Kill(pidInt, syscall.SIGKILL)
}
os.Remove(stateFile)
fmt.Fprintln(os.Stderr, "Stopped recording")
return
}
// Start new stream - optimized for tiny model (faster, lower latency)
cmd := exec.Command("whisper-stream", "-m", modelPath, "-t", "4", "--step", "500", "--length", "5000")
// Read from both stdout and stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
// Save PID
os.WriteFile(stateFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
// Open acme window if winid is set
var win *acme.Win
if winid != "" {
id, _ := strconv.Atoi(winid)
win, err = acme.Open(id, nil)
if err != nil {
log.Printf("Failed to open acme window: %v", err)
win = nil
}
}
// Process output from both pipes
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
// Merge stdout and stderr
combined := make(chan string, 100)
done := make(chan bool, 2)
go func() {
readLines(stdout, combined)
done <- true
}()
go func() {
readLines(stderr, combined)
done <- true
}()
// Close combined when both readers finish
go func() {
<-done
<-done
close(combined)
}()
lastLine := ""
for line := range combined {
part := ansiRegex.ReplaceAllString(line, "")
part = strings.TrimSpace(part)
// Filter out debug/empty lines and hallucinations
if part == "" || strings.HasPrefix(part, "whisper_") ||
strings.HasPrefix(part, "main:") || strings.HasPrefix(part, "init:") ||
part == "[BLANK_AUDIO]" || part == "[Start speaking]" ||
strings.HasPrefix(part, "(") || strings.HasPrefix(part, "[Music]") {
continue
}
// Only write if different from last line
if part != lastLine {
lastLine = part
// Write to acme or stdout
if win != nil {
win.Write("body", []byte(part+" "))
} else {
fmt.Println(part)
}
}
}
cmd.Wait()
os.Remove(stateFile)
if win != nil {
win.CloseFiles()
}
}