143 lines
4.9 KiB
Go
143 lines
4.9 KiB
Go
package transcriber
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/leonardotrapani/hyprvoice/internal/models/whisper"
|
|
"github.com/leonardotrapani/hyprvoice/internal/provider"
|
|
"github.com/leonardotrapani/hyprvoice/internal/recording"
|
|
)
|
|
|
|
// Main transcriber interface
|
|
type Transcriber interface {
|
|
Start(ctx context.Context, frameCh <-chan recording.AudioFrame) (<-chan error, error)
|
|
Stop(ctx context.Context) error
|
|
GetFinalTranscription() (string, error)
|
|
}
|
|
|
|
// BatchAdapter interface for batch transcription backends (collect all audio, transcribe at end)
|
|
type BatchAdapter interface {
|
|
Transcribe(ctx context.Context, audioData []byte) (string, error)
|
|
}
|
|
|
|
// Configuration for the transcriber
|
|
type Config struct {
|
|
Provider string
|
|
APIKey string
|
|
Language string
|
|
Model string
|
|
Keywords []string
|
|
Threads int // CPU threads for local transcription (0 = auto)
|
|
Streaming bool // use streaming mode if model supports it
|
|
}
|
|
|
|
// NewTranscriber creates a new transcriber based on model metadata
|
|
func NewTranscriber(config Config) (Transcriber, error) {
|
|
if config.Provider == "" {
|
|
return nil, fmt.Errorf("provider is required")
|
|
}
|
|
|
|
// map config provider name to registry provider name
|
|
registryProvider := provider.BaseProviderName(config.Provider)
|
|
|
|
// lookup provider
|
|
p := provider.GetProvider(registryProvider)
|
|
if p == nil {
|
|
return nil, fmt.Errorf("unknown provider: %s", config.Provider)
|
|
}
|
|
|
|
// check API key requirement
|
|
if p.RequiresAPIKey() && config.APIKey == "" {
|
|
return nil, fmt.Errorf("%s API key required", cases.Title(language.English).String(registryProvider))
|
|
}
|
|
|
|
// lookup model from provider
|
|
model, err := provider.GetModel(registryProvider, config.Model)
|
|
if err != nil {
|
|
// if model not found, try to use default model
|
|
if config.Model == "" {
|
|
defaultModel := p.DefaultModel(provider.Transcription)
|
|
if defaultModel != "" {
|
|
model, err = provider.GetModel(registryProvider, defaultModel)
|
|
}
|
|
}
|
|
if err != nil || model == nil {
|
|
return nil, fmt.Errorf("model not found: %s (provider: %s)", config.Model, config.Provider)
|
|
}
|
|
}
|
|
|
|
// check model type
|
|
if model.Type != provider.Transcription {
|
|
return nil, fmt.Errorf("model %s is not a transcription model", config.Model)
|
|
}
|
|
|
|
// runtime language-model compatibility check with fallback
|
|
// primary validation happens at config time (hard error), this is a safety net
|
|
if config.Language != "" && !model.SupportsLanguage(config.Language) {
|
|
log.Printf("warning: model %s does not support language %s, falling back to auto-detect", model.ID, config.Language)
|
|
config.Language = ""
|
|
}
|
|
|
|
// determine if we should use streaming mode
|
|
useStreaming := config.Streaming && model.SupportsStreaming
|
|
|
|
// fail if streaming-only model is used without streaming enabled
|
|
if !useStreaming && !model.SupportsBatch {
|
|
return nil, fmt.Errorf("model %s requires streaming mode (set streaming = true in config)", model.ID)
|
|
}
|
|
|
|
// streaming mode: use StreamingTranscriber
|
|
if useStreaming {
|
|
// pick the right adapter type for streaming
|
|
adapterType := model.AdapterType
|
|
if model.StreamingAdapter != "" {
|
|
adapterType = model.StreamingAdapter
|
|
}
|
|
|
|
// pick the right endpoint for streaming
|
|
endpoint := model.Endpoint
|
|
if model.StreamingEndpoint != nil {
|
|
endpoint = model.StreamingEndpoint
|
|
}
|
|
|
|
var streamingAdapter StreamingAdapter
|
|
switch adapterType {
|
|
case provider.AdapterElevenLabsStream:
|
|
streamingAdapter = NewElevenLabsStreamingAdapter(endpoint, config.APIKey, model.ID, config.Language, config.Keywords)
|
|
case provider.AdapterDeepgram:
|
|
streamingAdapter = NewDeepgramAdapter(endpoint, config.APIKey, model.ID, config.Language, config.Keywords)
|
|
case provider.AdapterOpenAIRealtime:
|
|
streamingAdapter = NewOpenAIRealtimeAdapter(endpoint, config.APIKey, model.ID, config.Language, config.Keywords)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported streaming adapter type: %s", adapterType)
|
|
}
|
|
return NewStreamingTranscriber(streamingAdapter, config.Language), nil
|
|
}
|
|
|
|
// batch mode: use SimpleTranscriber
|
|
var adapter BatchAdapter
|
|
switch model.AdapterType {
|
|
case provider.AdapterOpenAI:
|
|
adapter = NewOpenAIAdapter(model.Endpoint, config.APIKey, model.ID, config.Language, config.Keywords, registryProvider)
|
|
case provider.AdapterElevenLabs:
|
|
adapter = NewElevenLabsAdapter(model.Endpoint, config.APIKey, model.ID, config.Language, config.Keywords)
|
|
case provider.AdapterDeepgram:
|
|
adapter = NewDeepgramBatchAdapter(model.Endpoint, config.APIKey, model.ID, config.Language, config.Keywords)
|
|
case provider.AdapterWhisperCpp:
|
|
modelPath := whisper.GetModelPath(config.Model)
|
|
if modelPath == "" {
|
|
return nil, fmt.Errorf("unknown whisper model: %s", config.Model)
|
|
}
|
|
adapter = NewWhisperCppAdapter(modelPath, config.Language, config.Threads)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported adapter type: %s", model.AdapterType)
|
|
}
|
|
|
|
return NewSimpleTranscriber(config, adapter), nil
|
|
}
|