feat: refactor

This commit is contained in:
leonardotrapani
2026-02-01 17:24:43 +01:00
parent 13b1de4e04
commit 8df3021a9d
33 changed files with 1290 additions and 1577 deletions
+61 -7
View File
@@ -45,6 +45,43 @@ type Pipeline interface {
GetNotifyCh() <-chan notify.MessageType
}
// Factory types for dependency injection
type RecorderFactory func(cfg recording.Config) recording.Recorder
type TranscriberFactory func(cfg transcriber.Config) (transcriber.Transcriber, error)
type InjectorFactory func(cfg injection.Config) injection.Injector
type LLMAdapterFactory func(cfg llm.Config) (llm.Adapter, error)
// Option configures the pipeline
type Option func(*pipeline)
// WithRecorderFactory sets a custom recorder factory
func WithRecorderFactory(f RecorderFactory) Option {
return func(p *pipeline) {
p.recorderFactory = f
}
}
// WithTranscriberFactory sets a custom transcriber factory
func WithTranscriberFactory(f TranscriberFactory) Option {
return func(p *pipeline) {
p.transcriberFactory = f
}
}
// WithInjectorFactory sets a custom injector factory
func WithInjectorFactory(f InjectorFactory) Option {
return func(p *pipeline) {
p.injectorFactory = f
}
}
// WithLLMAdapterFactory sets a custom LLM adapter factory
func WithLLMAdapterFactory(f LLMAdapterFactory) Option {
return func(p *pipeline) {
p.llmAdapterFactory = f
}
}
type pipeline struct {
status Status
actionCh chan Action
@@ -58,15 +95,32 @@ type pipeline struct {
stopOnce sync.Once
running atomic.Bool
// dependency factories (for testing)
recorderFactory RecorderFactory
transcriberFactory TranscriberFactory
injectorFactory InjectorFactory
llmAdapterFactory LLMAdapterFactory
}
func New(cfg *config.Config) Pipeline {
return &pipeline{
func New(cfg *config.Config, opts ...Option) Pipeline {
p := &pipeline{
actionCh: make(chan Action, 1),
errorCh: make(chan PipelineError, 10),
notifyCh: make(chan notify.MessageType, 10),
config: cfg,
// default factories
recorderFactory: recording.NewRecorder,
transcriberFactory: transcriber.NewTranscriber,
injectorFactory: injection.NewInjector,
llmAdapterFactory: llm.NewAdapter,
}
for _, opt := range opts {
opt(p)
}
return p
}
func (p *pipeline) Run(ctx context.Context) {
if !p.running.CompareAndSwap(false, true) {
@@ -91,7 +145,7 @@ func (p *pipeline) run(ctx context.Context) {
log.Printf("Pipeline: Starting recording")
p.setStatus(Recording)
recorder := recording.NewRecorder(p.config.ToRecordingConfig())
recorder := p.recorderFactory(p.config.ToRecordingConfig())
frameCh, rErrCh, err := recorder.Start(ctx)
if err != nil {
@@ -102,7 +156,7 @@ func (p *pipeline) run(ctx context.Context) {
defer recorder.Stop()
t, err := transcriber.NewTranscriber(p.config.ToTranscriberConfig())
t, err := p.transcriberFactory(p.config.ToTranscriberConfig())
if err != nil {
log.Printf("Pipeline: Failed to create transcriber: %v", err)
p.sendError("Transcription Error", "Failed to create transcriber", err)
@@ -221,7 +275,7 @@ func (p *pipeline) sendNotify(mt notify.MessageType) {
}
}
func (p *pipeline) handleInjectAction(ctx context.Context, recorder *recording.Recorder, t transcriber.Transcriber) {
func (p *pipeline) handleInjectAction(ctx context.Context, recorder recording.Recorder, t transcriber.Transcriber) {
status := p.Status()
if status != Transcribing {
@@ -254,7 +308,7 @@ func (p *pipeline) handleInjectAction(ctx context.Context, recorder *recording.R
log.Printf("Pipeline: LLM post-processing enabled, processing text")
llmCfg := p.config.ToLLMConfig()
adapter, err := llm.NewAdapter(llm.Config{
adapter, err := p.llmAdapterFactory(llm.Config{
Provider: llmCfg.Provider,
APIKey: llmCfg.APIKey,
Model: llmCfg.Model,
@@ -279,7 +333,7 @@ func (p *pipeline) handleInjectAction(ctx context.Context, recorder *recording.R
p.setStatus(Injecting)
}
injector := injection.NewInjector(p.config.ToInjectionConfig())
injector := p.injectorFactory(p.config.ToInjectionConfig())
if err := injector.Inject(ctx, textToInject); err != nil {
p.sendError("Injection Error", "Failed to inject text", err)
+136
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/leonardotrapani/hyprvoice/internal/config"
"github.com/leonardotrapani/hyprvoice/internal/testutil"
)
func TestNew(t *testing.T) {
@@ -377,3 +378,138 @@ func TestPipeline_ConcurrentAccess(t *testing.T) {
<-done
<-done
}
func TestPipeline_WithMocks(t *testing.T) {
cfg := &config.Config{
Recording: config.RecordingConfig{
SampleRate: 16000,
Channels: 1,
Format: "s16",
BufferSize: 8192,
ChannelBufferSize: 30,
Timeout: 5 * time.Minute,
},
Transcription: config.TranscriptionConfig{
Provider: "openai",
APIKey: "test-key",
Language: "en",
Model: "whisper-1",
},
Injection: config.InjectionConfig{
Backends: []string{"clipboard"},
ClipboardTimeout: 3 * time.Second,
},
Notifications: config.NotificationsConfig{
Enabled: true,
Type: "log",
},
}
mockRecorder := testutil.NewMockRecorder()
mockTranscriber := testutil.NewMockTranscriber("hello world")
mockInjector := testutil.NewMockInjector()
p := New(cfg,
WithRecorderFactory(testutil.MockRecorderFactory(mockRecorder)),
WithTranscriberFactory(testutil.MockTranscriberFactory(mockTranscriber)),
WithInjectorFactory(testutil.MockInjectorFactory(mockInjector)),
)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
p.Run(ctx)
// wait for pipeline to start recording/transcribing
time.Sleep(50 * time.Millisecond)
// send inject action
p.GetActionCh() <- Inject
// wait for injection to complete
time.Sleep(100 * time.Millisecond)
// verify injection happened
injected := mockInjector.GetInjectedTexts()
if len(injected) != 1 {
t.Errorf("expected 1 injected text, got %d", len(injected))
} else if injected[0] != "hello world" {
t.Errorf("expected injected text 'hello world', got %q", injected[0])
}
p.Stop()
}
func TestPipeline_WithMocks_LLMProcessing(t *testing.T) {
cfg := &config.Config{
Recording: config.RecordingConfig{
SampleRate: 16000,
Channels: 1,
Format: "s16",
BufferSize: 8192,
ChannelBufferSize: 30,
Timeout: 5 * time.Minute,
},
Transcription: config.TranscriptionConfig{
Provider: "openai",
APIKey: "test-key",
Language: "en",
Model: "whisper-1",
},
Injection: config.InjectionConfig{
Backends: []string{"clipboard"},
ClipboardTimeout: 3 * time.Second,
},
Notifications: config.NotificationsConfig{
Enabled: true,
Type: "log",
},
LLM: config.LLMConfig{
Enabled: true,
Provider: "openai",
Model: "gpt-4",
},
Providers: map[string]config.ProviderConfig{
"openai": {APIKey: "test-key"},
},
}
mockRecorder := testutil.NewMockRecorder()
mockTranscriber := testutil.NewMockTranscriber("um hello um world")
mockInjector := testutil.NewMockInjector()
mockLLM := testutil.NewMockLLMAdapter("Hello, World!")
p := New(cfg,
WithRecorderFactory(testutil.MockRecorderFactory(mockRecorder)),
WithTranscriberFactory(testutil.MockTranscriberFactory(mockTranscriber)),
WithInjectorFactory(testutil.MockInjectorFactory(mockInjector)),
WithLLMAdapterFactory(testutil.MockLLMAdapterFactory(mockLLM)),
)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
p.Run(ctx)
time.Sleep(50 * time.Millisecond)
p.GetActionCh() <- Inject
time.Sleep(100 * time.Millisecond)
// verify LLM was called with transcription
if !mockLLM.ProcessCalled {
t.Error("expected LLM.Process to be called")
}
if mockLLM.InputText != "um hello um world" {
t.Errorf("expected LLM input 'um hello um world', got %q", mockLLM.InputText)
}
// verify injection used LLM output
injected := mockInjector.GetInjectedTexts()
if len(injected) != 1 {
t.Errorf("expected 1 injected text, got %d", len(injected))
} else if injected[0] != "Hello, World!" {
t.Errorf("expected injected text 'Hello, World!', got %q", injected[0])
}
p.Stop()
}