mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
// Package openai implements stt.Transcriber against the OpenAI
|
|
// /audio/transcriptions endpoint (and any compatible third-party endpoint
|
|
// such as Groq Whisper, faster-whisper self-hosted, or Azure Whisper).
|
|
package openai
|
|
|
|
import (
|
|
"context"
|
|
"mime"
|
|
"net/url"
|
|
"strings"
|
|
|
|
openaisdk "github.com/openai/openai-go/v3"
|
|
openaioption "github.com/openai/openai-go/v3/option"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/usememos/memos/internal/ai"
|
|
"github.com/usememos/memos/internal/ai/stt"
|
|
)
|
|
|
|
const defaultEndpoint = "https://api.openai.com/v1"
|
|
|
|
// Transcriber implements stt.Transcriber for OpenAI-compatible STT endpoints.
|
|
type Transcriber struct {
|
|
client openaisdk.Client
|
|
}
|
|
|
|
// New constructs a Transcriber from a provider config.
|
|
func New(cfg ai.ProviderConfig, options stt.Options) (*Transcriber, error) {
|
|
endpoint, err := normalizeEndpoint(cfg.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.APIKey == "" {
|
|
return nil, errors.New("OpenAI API key is required")
|
|
}
|
|
return &Transcriber{
|
|
client: openaisdk.NewClient(
|
|
openaioption.WithAPIKey(cfg.APIKey),
|
|
openaioption.WithBaseURL(endpoint),
|
|
openaioption.WithHTTPClient(options.HTTPClient),
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
// Transcribe sends the audio to /audio/transcriptions.
|
|
func (t *Transcriber) Transcribe(ctx context.Context, req stt.Request) (*stt.Response, error) {
|
|
if strings.TrimSpace(req.Model) == "" {
|
|
return nil, errors.New("model is required")
|
|
}
|
|
if req.Audio == nil {
|
|
return nil, errors.New("audio is required")
|
|
}
|
|
|
|
filename, contentType, err := normalizeAudioMetadata(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := openaisdk.AudioTranscriptionNewParams{
|
|
File: openaisdk.File(req.Audio, filename, contentType),
|
|
Model: openaisdk.AudioModel(req.Model),
|
|
ResponseFormat: openaisdk.AudioResponseFormatJSON,
|
|
}
|
|
if req.Prompt != "" {
|
|
params.Prompt = openaisdk.String(req.Prompt)
|
|
}
|
|
if req.Language != "" {
|
|
params.Language = openaisdk.String(req.Language)
|
|
}
|
|
|
|
resp, err := t.client.Audio.Transcriptions.New(ctx, params)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to send OpenAI transcription request")
|
|
}
|
|
return &stt.Response{
|
|
Text: resp.Text,
|
|
Language: resp.Language,
|
|
}, nil
|
|
}
|
|
|
|
func normalizeEndpoint(endpoint string) (string, error) {
|
|
endpoint = strings.TrimSpace(endpoint)
|
|
if endpoint == "" {
|
|
endpoint = defaultEndpoint
|
|
}
|
|
if _, err := url.ParseRequestURI(endpoint); err != nil {
|
|
return "", errors.Wrap(err, "invalid OpenAI endpoint")
|
|
}
|
|
return strings.TrimRight(endpoint, "/"), nil
|
|
}
|
|
|
|
func normalizeAudioMetadata(req stt.Request) (string, string, error) {
|
|
filename := strings.TrimSpace(req.Filename)
|
|
if filename == "" {
|
|
filename = "audio"
|
|
}
|
|
contentType := strings.TrimSpace(req.ContentType)
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
} else {
|
|
mediaType, _, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "invalid audio content type")
|
|
}
|
|
contentType = mediaType
|
|
}
|
|
return sanitizeFilename(filename), contentType, nil
|
|
}
|
|
|
|
func sanitizeFilename(filename string) string {
|
|
filename = strings.NewReplacer("\r", "_", "\n", "_").Replace(filename)
|
|
if strings.TrimSpace(filename) == "" {
|
|
return "audio"
|
|
}
|
|
return filename
|
|
}
|