Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/api/chat_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (h *Handler) getLLMService(ctx context.Context) (llm.Service, string, error
if cfg.APIKey == nil || *cfg.APIKey == "" {
return nil, cfg.Provider, fmt.Errorf("OpenAI API key not configured")
}
return llm.NewOpenAIService(*cfg.APIKey), cfg.Provider, nil
return llm.NewOpenAIService(*cfg.APIKey, cfg.OpenAIBaseURL), cfg.Provider, nil
case "ollama":
if cfg.BaseURL == nil || *cfg.BaseURL == "" {
return nil, cfg.Provider, fmt.Errorf("Ollama base URL not configured")
Expand Down
86 changes: 52 additions & 34 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,23 @@ type YouTubeDownloadResponse struct {

// LLMConfigRequest represents the LLM configuration request
type LLMConfigRequest struct {
Provider string `json:"provider" binding:"required,oneof=ollama openai"`
BaseURL *string `json:"base_url,omitempty"`
APIKey *string `json:"api_key,omitempty"`
IsActive bool `json:"is_active"`
Provider string `json:"provider" binding:"required,oneof=ollama openai"`
BaseURL *string `json:"base_url,omitempty"`
OpenAIBaseURL *string `json:"openai_base_url,omitempty"`
APIKey *string `json:"api_key,omitempty"`
IsActive bool `json:"is_active"`
}

// LLMConfigResponse represents the LLM configuration response
type LLMConfigResponse struct {
ID uint `json:"id"`
Provider string `json:"provider"`
BaseURL *string `json:"base_url,omitempty"`
HasAPIKey bool `json:"has_api_key"` // Don't return actual API key
IsActive bool `json:"is_active"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID uint `json:"id"`
Provider string `json:"provider"`
BaseURL *string `json:"base_url,omitempty"`
OpenAIBaseURL *string `json:"openai_base_url,omitempty"`
HasAPIKey bool `json:"has_api_key"` // Don't return actual API key
IsActive bool `json:"is_active"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

// APIKeyListResponse represents an API key in the list (without the actual key)
Expand Down Expand Up @@ -1880,13 +1882,14 @@ func (h *Handler) GetLLMConfig(c *gin.Context) {
}

response := LLMConfigResponse{
ID: config.ID,
Provider: config.Provider,
BaseURL: config.BaseURL,
HasAPIKey: config.APIKey != nil && *config.APIKey != "",
IsActive: config.IsActive,
CreatedAt: config.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"),
ID: config.ID,
Provider: config.Provider,
BaseURL: config.BaseURL,
OpenAIBaseURL: config.OpenAIBaseURL,
HasAPIKey: config.APIKey != nil && *config.APIKey != "",
IsActive: config.IsActive,
CreatedAt: config.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"),
}

c.JSON(http.StatusOK, response)
Expand Down Expand Up @@ -1914,10 +1917,6 @@ func (h *Handler) SaveLLMConfig(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Base URL is required for Ollama provider"})
return
}
if req.Provider == "openai" && (req.APIKey == nil || *req.APIKey == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "API key is required for OpenAI provider"})
return
}

// Check if there's an existing active configuration
existingConfig, err := h.llmConfigRepo.GetActive(c.Request.Context())
Expand All @@ -1926,15 +1925,32 @@ func (h *Handler) SaveLLMConfig(c *gin.Context) {
return
}

// Handle API Key logic for OpenAI
var apiKeyToSave *string
if req.Provider == "openai" {
if req.APIKey != nil && *req.APIKey != "" {
// New key provided
apiKeyToSave = req.APIKey
} else if existingConfig != nil && existingConfig.APIKey != nil && *existingConfig.APIKey != "" {
// Reuse existing key
apiKeyToSave = existingConfig.APIKey
} else {
// No key provided and no existing key
c.JSON(http.StatusBadRequest, gin.H{"error": "API key is required for OpenAI provider"})
return
}
}

var config *models.LLMConfig

if err == gorm.ErrRecordNotFound {
// No existing active config, create new one
config = &models.LLMConfig{
Provider: req.Provider,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
IsActive: req.IsActive,
Provider: req.Provider,
BaseURL: req.BaseURL,
OpenAIBaseURL: req.OpenAIBaseURL,
APIKey: apiKeyToSave,
IsActive: req.IsActive,
}

if err := h.llmConfigRepo.Create(c.Request.Context(), config); err != nil {
Expand All @@ -1945,7 +1961,8 @@ func (h *Handler) SaveLLMConfig(c *gin.Context) {
// Update existing config
existingConfig.Provider = req.Provider
existingConfig.BaseURL = req.BaseURL
existingConfig.APIKey = req.APIKey
existingConfig.OpenAIBaseURL = req.OpenAIBaseURL
existingConfig.APIKey = apiKeyToSave
existingConfig.IsActive = req.IsActive

if err := h.llmConfigRepo.Update(c.Request.Context(), existingConfig); err != nil {
Expand All @@ -1956,13 +1973,14 @@ func (h *Handler) SaveLLMConfig(c *gin.Context) {
}

response := LLMConfigResponse{
ID: config.ID,
Provider: config.Provider,
BaseURL: config.BaseURL,
HasAPIKey: config.APIKey != nil && *config.APIKey != "",
IsActive: config.IsActive,
CreatedAt: config.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"),
ID: config.ID,
Provider: config.Provider,
BaseURL: config.BaseURL,
OpenAIBaseURL: config.OpenAIBaseURL,
HasAPIKey: config.APIKey != nil && *config.APIKey != "",
IsActive: config.IsActive,
CreatedAt: config.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"),
}

c.JSON(http.StatusOK, response)
Expand Down
8 changes: 6 additions & 2 deletions internal/llm/openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ type OpenAIService struct {
}

// NewOpenAIService creates a new OpenAI service
func NewOpenAIService(apiKey string) *OpenAIService {
func NewOpenAIService(apiKey string, baseURL *string) *OpenAIService {
url := "https://api.openai.com/v1"
if baseURL != nil && *baseURL != "" {
url = *baseURL
}
return &OpenAIService{
apiKey: apiKey,
baseURL: "https://api.openai.com/v1",
baseURL: url,
client: &http.Client{
Timeout: 300 * time.Second,
},
Expand Down
15 changes: 8 additions & 7 deletions internal/models/transcription.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,14 @@ func (tp *TranscriptionProfile) BeforeSave(tx *gorm.DB) error {

// LLMConfig represents LLM configuration settings
type LLMConfig struct {
ID uint `json:"id" gorm:"primaryKey"`
Provider string `json:"provider" gorm:"not null;type:varchar(50)"` // "ollama" or "openai"
BaseURL *string `json:"base_url,omitempty" gorm:"type:text"` // For Ollama
APIKey *string `json:"api_key,omitempty" gorm:"type:text"` // For OpenAI (encrypted)
IsActive bool `json:"is_active" gorm:"type:boolean;default:false"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ID uint `json:"id" gorm:"primaryKey"`
Provider string `json:"provider" gorm:"not null;type:varchar(50)"` // "ollama" or "openai"
BaseURL *string `json:"base_url,omitempty" gorm:"type:text"` // For Ollama
OpenAIBaseURL *string `json:"openai_base_url,omitempty" gorm:"type:text"` // For OpenAI custom endpoint
APIKey *string `json:"api_key,omitempty" gorm:"type:text"` // For OpenAI (encrypted)
IsActive bool `json:"is_active" gorm:"type:boolean;default:false"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}

// BeforeSave ensures only one LLM config can be active
Expand Down
32 changes: 26 additions & 6 deletions internal/transcription/adapters/whisperx_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Name: "model",
Type: "string",
Required: false,
Default: "small",
Options: []string{"tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large", "large-v1", "large-v2", "large-v3"},
Default: "KBLab/kb-whisper-large",
Options: []string{"KBLab/kb-whisper-large","KBLab/kb-whisper-medium","KBLab/kb-whisper-small","KBLab/kb-whisper-base","KBLab/kb-whisper-tiny","tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large", "large-v1", "large-v2", "large-v3"},
Description: "Whisper model size to use",
Group: "basic",
},
Expand All @@ -75,7 +75,7 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Name: "device",
Type: "string",
Required: false,
Default: "cpu",
Default: "cuda",
Options: []string{"cpu", "cuda"},
Description: "Device to use for computation",
Group: "basic",
Expand Down Expand Up @@ -104,7 +104,7 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Name: "compute_type",
Type: "string",
Required: false,
Default: "float32",
Default: "float16",
Options: []string{"float16", "float32", "int8"},
Description: "Computation precision",
Group: "advanced",
Expand All @@ -125,7 +125,7 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Name: "language",
Type: "string",
Required: false,
Default: nil,
Default: "sv",
Description: "Language code (auto-detect if not specified)",
Group: "basic",
},
Expand All @@ -144,7 +144,7 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Name: "diarize",
Type: "bool",
Required: false,
Default: false,
Default: true,
Description: "Enable speaker diarization",
Group: "basic",
},
Expand Down Expand Up @@ -258,6 +258,16 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
Description: "VAD offset threshold",
Group: "advanced",
},

// Custom Alignment Model
{
Name: "align_model",
Type: "string",
Required: false,
Default: nil,
Description: "Custom alignment model (e.g. KBLab/wav2vec2-large-voxrex-swedish)",
Group: "advanced",
},
}

baseAdapter := NewBaseAdapter("whisperx", filepath.Join(envPath, "WhisperX"), capabilities, schema)
Expand All @@ -273,6 +283,11 @@ func NewWhisperXAdapter(envPath string) *WhisperXAdapter {
// GetSupportedModels returns the list of Whisper models supported
func (w *WhisperXAdapter) GetSupportedModels() []string {
return []string{
"KBLab/kb-whisper-large",
"KBLab/kb-whisper-medium",
"KBLab/kb-whisper-small",
"KBLab/kb-whisper-base",
"KBLab/kb-whisper-tiny",
"tiny", "tiny.en",
"base", "base.en",
"small", "small.en",
Expand Down Expand Up @@ -484,6 +499,11 @@ func (w *WhisperXAdapter) buildWhisperXArgs(input interfaces.AudioInput, params
args = append(args, "--vad_onset", fmt.Sprintf("%.3f", w.GetFloatParameter(params, "vad_onset")))
args = append(args, "--vad_offset", fmt.Sprintf("%.3f", w.GetFloatParameter(params, "vad_offset")))

// Custom alignment model
if alignModel := w.GetStringParameter(params, "align_model"); alignModel != "" {
args = append(args, "--align_model", alignModel)
}

// Diarization
if w.GetBoolParameter(params, "diarize") {
args = append(args, "--diarize")
Expand Down
67 changes: 45 additions & 22 deletions web/frontend/src/components/LLMSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface LLMConfig {
id?: number;
provider: string;
base_url?: string;
openai_base_url?: string;
has_api_key?: boolean;
is_active: boolean;
created_at?: string;
Expand All @@ -22,6 +23,7 @@ export function LLMSettings() {
is_active: false,
});
const [baseUrl, setBaseUrl] = useState("");
const [openAIBaseUrl, setOpenAIBaseUrl] = useState("");
const [apiKey, setApiKey] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
Expand All @@ -42,6 +44,7 @@ export function LLMSettings() {
const data = await response.json();
setConfig(data);
setBaseUrl(data.base_url || "");
setOpenAIBaseUrl(data.openai_base_url || "");
// Don't set API key from response for security
} else if (response.status !== 404) {
console.error("Failed to fetch LLM config");
Expand All @@ -61,7 +64,10 @@ export function LLMSettings() {
provider: config.provider,
is_active: true, // Always set to active when saving
...(config.provider === "ollama" && { base_url: baseUrl }),
...(config.provider === "openai" && { api_key: apiKey }),
...(config.provider === "openai" && {
api_key: apiKey,
openai_base_url: openAIBaseUrl
}),
};

try {
Expand Down Expand Up @@ -228,27 +234,44 @@ export function LLMSettings() {
)}

{config.provider === "openai" && (
<div>
<Label htmlFor="apiKey" className="flex items-center gap-2">
<Key className="h-4 w-4" />
OpenAI API Key *
{config.has_api_key && (
<span className="text-xs bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 px-2 py-1 rounded">
Already configured
</span>
)}
</Label>
<Input
id="apiKey"
type="password"
placeholder={config.has_api_key ? "Enter new API key to update" : "sk-..."}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="mt-1"
/>
<p className="text-xs text-carbon-500 dark:text-carbon-400 mt-1">
Your OpenAI API key. {config.has_api_key ? "Leave blank to keep current key." : ""}
</p>
<div className="space-y-4">
<div>
<Label htmlFor="apiKey" className="flex items-center gap-2">
<Key className="h-4 w-4" />
OpenAI API Key *
{config.has_api_key && (
<span className="text-xs bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 px-2 py-1 rounded">
Already configured
</span>
)}
</Label>
<Input
id="apiKey"
type="password"
placeholder={config.has_api_key ? "Enter new API key to update" : "sk-..."}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="mt-1"
/>
<p className="text-xs text-carbon-500 dark:text-carbon-400 mt-1">
Your OpenAI API key. {config.has_api_key ? "Leave blank to keep current key." : ""}
</p>
</div>

<div>
<Label htmlFor="openAIBaseUrl">OpenAI Base URL (Optional)</Label>
<Input
id="openAIBaseUrl"
type="url"
placeholder="https://api.openai.com/v1"
value={openAIBaseUrl}
onChange={(e) => setOpenAIBaseUrl(e.target.value)}
className="mt-1"
/>
<p className="text-xs text-carbon-500 dark:text-carbon-400 mt-1">
Custom endpoint URL for OpenAI-compatible services. Leave blank for default.
</p>
</div>
</div>
)}
</div>
Expand Down
Loading