refactor moving status to pipeline not daemon
This commit is contained in:
+24
-98
@@ -16,25 +16,14 @@ import (
|
|||||||
"github.com/leonardotrapani/hyprvoice/internal/pipeline"
|
"github.com/leonardotrapani/hyprvoice/internal/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status = pipeline.Status
|
|
||||||
|
|
||||||
const (
|
|
||||||
Idle = pipeline.Idle
|
|
||||||
Recording = pipeline.Recording
|
|
||||||
Transcribing = pipeline.Transcribing
|
|
||||||
Injecting = pipeline.Injecting
|
|
||||||
)
|
|
||||||
|
|
||||||
type Daemon struct {
|
type Daemon struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
status Status
|
|
||||||
notifier notify.Notifier
|
notifier notify.Notifier
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
pipeline pipeline.Pipeline
|
pipeline pipeline.Pipeline
|
||||||
actionChannel chan<- pipeline.Action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(n notify.Notifier) *Daemon {
|
func New(n notify.Notifier) *Daemon {
|
||||||
@@ -46,48 +35,16 @@ func New(n notify.Notifier) *Daemon {
|
|||||||
notifier: n,
|
notifier: n,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
status: Idle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) Status() Status {
|
func (d *Daemon) status() pipeline.Status {
|
||||||
d.mu.RLock()
|
if d.pipeline == nil {
|
||||||
defer d.mu.RUnlock()
|
return pipeline.Idle
|
||||||
return d.status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) startStatusReader(ctx context.Context, statusCh <-chan Status) {
|
|
||||||
defer func() {
|
|
||||||
// Always clean up when this goroutine exits
|
|
||||||
d.mu.Lock()
|
|
||||||
d.status = Idle
|
|
||||||
d.pipeline = nil
|
|
||||||
d.actionChannel = nil
|
|
||||||
d.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case status, ok := <-statusCh:
|
|
||||||
if !ok {
|
|
||||||
return // Channel closed
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mu.Lock()
|
|
||||||
oldStatus := d.status
|
|
||||||
d.status = status
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
if oldStatus != status {
|
|
||||||
log.Printf("Status changed: %s -> %s", oldStatus, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
return // Context cancelled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return d.pipeline.Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) Run() error {
|
func (d *Daemon) Run() error {
|
||||||
@@ -158,7 +115,7 @@ func (d *Daemon) handle(c net.Conn) {
|
|||||||
d.toggle()
|
d.toggle()
|
||||||
fmt.Fprint(c, "OK toggled\n")
|
fmt.Fprint(c, "OK toggled\n")
|
||||||
case 's':
|
case 's':
|
||||||
status := d.Status()
|
status := d.status()
|
||||||
fmt.Fprintf(c, "STATUS status=%s\n", status)
|
fmt.Fprintf(c, "STATUS status=%s\n", status)
|
||||||
case 'v':
|
case 'v':
|
||||||
fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer)
|
fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer)
|
||||||
@@ -172,64 +129,33 @@ func (d *Daemon) handle(c net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) toggle() {
|
func (d *Daemon) toggle() {
|
||||||
// Capture current state and prepare action under lock
|
|
||||||
d.mu.Lock()
|
|
||||||
status := d.status
|
|
||||||
var pipelineToStop pipeline.Pipeline
|
|
||||||
var actionChan chan<- pipeline.Action
|
var actionChan chan<- pipeline.Action
|
||||||
|
|
||||||
switch status {
|
switch d.status() {
|
||||||
case Idle:
|
case pipeline.Idle:
|
||||||
// Clean up any existing pipeline first
|
p := pipeline.New()
|
||||||
|
p.Run(d.ctx)
|
||||||
|
d.pipeline = p
|
||||||
|
go d.notifier.RecordingStarted()
|
||||||
|
|
||||||
|
case pipeline.Recording:
|
||||||
|
go d.notifier.Aborted()
|
||||||
if d.pipeline != nil {
|
if d.pipeline != nil {
|
||||||
pipelineToStop = d.pipeline
|
d.pipeline.Stop() // aborted during recording (chunks not sent to transcriber yet)
|
||||||
d.pipeline = nil
|
d.pipeline = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start new pipeline
|
case pipeline.Transcribing:
|
||||||
p := pipeline.New()
|
|
||||||
statusCh, actionCh := p.Run(d.ctx)
|
|
||||||
d.pipeline = p
|
|
||||||
d.actionChannel = actionCh
|
|
||||||
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
// Stop old pipeline if needed (outside lock)
|
|
||||||
if pipelineToStop != nil {
|
|
||||||
pipelineToStop.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
go d.notifier.RecordingStarted()
|
|
||||||
go d.startStatusReader(d.ctx, statusCh)
|
|
||||||
|
|
||||||
case Recording:
|
|
||||||
pipelineToStop = d.pipeline
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
go d.notifier.RecordingEnded()
|
go d.notifier.RecordingEnded()
|
||||||
if pipelineToStop != nil {
|
actionChan = d.pipeline.Actions()
|
||||||
pipelineToStop.Stop()
|
actionChan <- pipeline.Inject
|
||||||
}
|
|
||||||
|
|
||||||
case Transcribing:
|
case pipeline.Injecting:
|
||||||
actionChan = d.actionChannel
|
go d.notifier.Aborted()
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
// Try to inject (non-blocking)
|
if d.pipeline != nil {
|
||||||
if actionChan != nil {
|
d.pipeline.Stop() // aborted during injection
|
||||||
select {
|
d.pipeline = nil
|
||||||
case actionChan <- pipeline.Inject:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case Injecting:
|
|
||||||
pipelineToStop = d.pipeline
|
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
go d.notifier.RecordingEnded()
|
|
||||||
if pipelineToStop != nil {
|
|
||||||
pipelineToStop.Stop() // aborted during injection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
RecordingStarted()
|
RecordingStarted()
|
||||||
RecordingEnded()
|
RecordingEnded()
|
||||||
|
Aborted()
|
||||||
Transcribing()
|
Transcribing()
|
||||||
Error(msg string)
|
Error(msg string)
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,13 @@ func (Desktop) Transcribing() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Desktop) Aborted() {
|
||||||
|
cmd := exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", "Hyprvoice: Aborted")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Printf("Failed to send abort notification: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (Desktop) Error(msg string) {
|
func (Desktop) Error(msg string) {
|
||||||
cmd := exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", msg)
|
cmd := exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", msg)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Status string
|
type Status string
|
||||||
|
type Action string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Idle Status = "idle"
|
Idle Status = "idle"
|
||||||
@@ -18,21 +19,19 @@ const (
|
|||||||
Injecting Status = "injecting"
|
Injecting Status = "injecting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Action represents commands sent to the pipeline to drive state
|
|
||||||
// transitions or trigger work. Keeping this separate from Status
|
|
||||||
// preserves directionality: callers send Actions, pipeline emits Statuses.
|
|
||||||
type Action int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Inject Action = iota
|
Inject Action = "inject"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pipeline interface {
|
type Pipeline interface {
|
||||||
Run(ctx context.Context) (<-chan Status, chan<- Action)
|
Run(ctx context.Context)
|
||||||
Stop()
|
Stop()
|
||||||
|
Status() Status
|
||||||
|
Actions() chan<- Action
|
||||||
}
|
}
|
||||||
|
|
||||||
type pipeline struct {
|
type pipeline struct {
|
||||||
|
status Status
|
||||||
actionCh chan Action
|
actionCh chan Action
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -44,74 +43,75 @@ func New() Pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pipeline) Run(ctx context.Context) (<-chan Status, chan<- Action) {
|
func (p *pipeline) Status() Status {
|
||||||
statusCh := make(chan Status, 1)
|
return p.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipeline) Actions() chan<- Action {
|
||||||
|
return p.actionCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipeline) Stop() {
|
||||||
|
if p.cancel != nil {
|
||||||
|
p.cancel()
|
||||||
|
}
|
||||||
|
p.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipeline) Run(ctx context.Context) {
|
||||||
runCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
runCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
p.cancel = cancel
|
p.cancel = cancel
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
go p.run(runCtx, statusCh)
|
go p.run(runCtx)
|
||||||
return statusCh, p.actionCh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pipeline) run(ctx context.Context, statusCh chan<- Status) {
|
func (p *pipeline) run(ctx context.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
// Ensure status channel is closed and wg is decremented
|
|
||||||
close(statusCh)
|
|
||||||
p.wg.Done()
|
p.wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("Pipeline: Starting recording")
|
log.Printf("Pipeline: Starting recording")
|
||||||
statusCh <- Recording
|
p.status = Recording
|
||||||
|
|
||||||
recorder := recording.NewDefaultRecorder()
|
recorder := recording.NewDefaultRecorder()
|
||||||
frameCh, errCh, err := recorder.Start(ctx)
|
frameCh, errCh, err := recorder.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Pipeline: Recording error: %v", err)
|
log.Printf("Pipeline: Recording error: %v", err)
|
||||||
statusCh <- Idle
|
p.status = Idle
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer recorder.Stop()
|
defer recorder.Stop()
|
||||||
|
|
||||||
// Track statistics for logging
|
|
||||||
frameCount := 0
|
frameCount := 0
|
||||||
totalBytes := 0
|
totalBytes := 0
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
// Main event loop
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case frame, ok := <-frameCh:
|
case frame := <-frameCh:
|
||||||
if !ok {
|
|
||||||
// Frame channel closed, recording ended
|
|
||||||
log.Printf("Pipeline: Recording ended. Total frames: %d, Total bytes: %d, Duration: %v",
|
|
||||||
frameCount, totalBytes, time.Since(startTime))
|
|
||||||
statusCh <- Transcribing
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Log frame details
|
|
||||||
frameCount++
|
frameCount++
|
||||||
totalBytes += len(frame.Data)
|
totalBytes += len(frame.Data)
|
||||||
log.Printf("Pipeline: Received frame #%d - Size: %d bytes, Timestamp: %v, Total bytes so far: %d",
|
log.Printf("Pipeline: Received frame #%d - Size: %d bytes, Timestamp: %v, Total bytes so far: %d",
|
||||||
frameCount, len(frame.Data), frame.Timestamp.Format("15:04:05.000"), totalBytes)
|
frameCount, len(frame.Data), frame.Timestamp.Format("15:04:05.000"), totalBytes)
|
||||||
// TODO: pass this channel to the transcriber
|
|
||||||
|
p.status = Transcribing
|
||||||
|
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Pipeline: Recording error: %v", err)
|
log.Printf("Pipeline: Recording error: %v", err)
|
||||||
statusCh <- Idle
|
p.status = Idle
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case action := <-p.actionCh:
|
case action := <-p.actionCh:
|
||||||
log.Printf("Pipeline: Received action: %v", action)
|
log.Printf("Pipeline: Received action: %v", action)
|
||||||
if action == Inject {
|
switch action {
|
||||||
log.Printf("Pipeline: Inject action received, stopping recording")
|
case Inject:
|
||||||
|
log.Printf("Pipeline: Inject action received, stopping recording (TODO: stop transcribing)")
|
||||||
|
|
||||||
if err := recorder.Stop(); err != nil {
|
if err := recorder.Stop(); err != nil {
|
||||||
log.Printf("Pipeline: Error stopping recorder: %v", err)
|
log.Printf("Pipeline: Error stopping recorder: %v", err)
|
||||||
}
|
}
|
||||||
statusCh <- Injecting
|
p.status = Injecting
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,10 +121,3 @@ func (p *pipeline) run(ctx context.Context, statusCh chan<- Status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pipeline) Stop() {
|
|
||||||
if p.cancel != nil {
|
|
||||||
p.cancel()
|
|
||||||
}
|
|
||||||
p.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ func (r *Recorder) Start(ctx context.Context) (<-chan AudioFrame, <-chan error,
|
|||||||
return nil, nil, fmt.Errorf("PipeWire not available: %w", err)
|
return nil, nil, fmt.Errorf("PipeWire not available: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cancellable context specific to this recording session.
|
|
||||||
recordingCtx, cancel := context.WithCancel(ctx)
|
recordingCtx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
frameCh := make(chan AudioFrame, r.config.ChannelBufferSize)
|
frameCh := make(chan AudioFrame, r.config.ChannelBufferSize)
|
||||||
@@ -150,7 +149,6 @@ func (r *Recorder) captureLoop(ctx context.Context, frameCh chan<- AudioFrame, e
|
|||||||
r.cmd = cmd
|
r.cmd = cmd
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
// Log stderr lines to aid diagnostics.
|
|
||||||
go func() {
|
go func() {
|
||||||
scanner := bufio.NewScanner(stderr)
|
scanner := bufio.NewScanner(stderr)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -158,47 +156,46 @@ func (r *Recorder) captureLoop(ctx context.Context, frameCh chan<- AudioFrame, e
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buffer := make([]byte, r.config.BufferSize)
|
|
||||||
var sentCount int
|
|
||||||
var droppedCount int
|
|
||||||
lastDropLog := time.Now()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, readErr := stdout.Read(buffer)
|
|
||||||
if n > 0 {
|
|
||||||
frameData := make([]byte, n)
|
|
||||||
copy(frameData, buffer[:n])
|
|
||||||
|
|
||||||
frame := AudioFrame{Data: frameData, Timestamp: time.Now()}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case frameCh <- frame:
|
|
||||||
sentCount++
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
droppedCount++
|
|
||||||
if time.Since(lastDropLog) > time.Second {
|
|
||||||
log.Printf("Recording: dropped %d frames due to backpressure", droppedCount)
|
|
||||||
lastDropLog = time.Now()
|
|
||||||
droppedCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if readErr != nil {
|
|
||||||
if errors.Is(readErr, io.EOF) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.emitErr(errCh, fmt.Errorf("read audio: %w", readErr))
|
|
||||||
r.requestCancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
|
buffer := make([]byte, r.config.BufferSize)
|
||||||
|
var sentCount int
|
||||||
|
var droppedCount int
|
||||||
|
lastDropLog := time.Now()
|
||||||
|
n, readErr := stdout.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
frameData := make([]byte, n)
|
||||||
|
copy(frameData, buffer[:n])
|
||||||
|
|
||||||
|
frame := AudioFrame{Data: frameData, Timestamp: time.Now()}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case frameCh <- frame:
|
||||||
|
sentCount++
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
droppedCount++
|
||||||
|
if time.Since(lastDropLog) > time.Second {
|
||||||
|
log.Printf("Recording: dropped %d frames due to backpressure", droppedCount)
|
||||||
|
lastDropLog = time.Now()
|
||||||
|
droppedCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if readErr != nil {
|
||||||
|
if errors.Is(readErr, io.EOF) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.emitErr(errCh, fmt.Errorf("read audio: %w", readErr))
|
||||||
|
r.requestCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user