Some checks failed
CodeQL / Analyze (go) (push) Successful in 6m28s
Docker Image / build-docker (push) Failing after 13m26s
Lint and Testing / lint (push) Successful in 11m17s
Lint and Testing / test (push) Successful in 11m17s
Lint and Testing / golangci (push) Successful in 2m40s
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package transcode
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// Engine converts input audio to PCM WAV using pure Go decoders (no ffmpeg).
|
|
type Engine struct{}
|
|
|
|
// NewEngine creates a transcoder. The ffmpegPath argument is ignored (kept for config compatibility).
|
|
func NewEngine(_ string) *Engine {
|
|
return &Engine{}
|
|
}
|
|
|
|
func (e *Engine) Available() error {
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) Transcode(ctx context.Context, src, dst string, opt Options) error {
|
|
if err := opt.Validate(); err != nil {
|
|
return err
|
|
}
|
|
spec, err := ResolveFormat(opt.Format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dst, err = OutputPath(dst, spec.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
streamer, format, closer, err := openDecoder(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer.Close()
|
|
s, format := buildPipeline(streamer, format, opt)
|
|
samples, err := drainSamples(ctx, s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch := format.NumChannels
|
|
if opt.Channels > 0 {
|
|
ch = opt.Channels
|
|
}
|
|
sr := int(format.SampleRate)
|
|
if opt.SampleRate > 0 {
|
|
sr = opt.SampleRate
|
|
}
|
|
if err := writePCM16WAV(dst, sr, ch, samples); err != nil {
|
|
return fmt.Errorf("write wav: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Transcode(ctx context.Context, src, dst string, opt Options) error {
|
|
return NewEngine("").Transcode(ctx, src, dst, opt)
|
|
}
|
|
|
|
func ToWhisperWAV(ctx context.Context, src, dst string) error {
|
|
return Transcode(ctx, src, dst, WhisperOptions())
|
|
}
|
|
|
|
// SupportedInputFormats lists file extensions decoded without external tools.
|
|
func SupportedInputFormats() []string {
|
|
return append([]string(nil), probeFormats...)
|
|
}
|
|
|
|
func (e *Engine) Probe(ctx context.Context, path string) (MediaInfo, error) {
|
|
_ = ctx
|
|
ext, err := resolveFormat(path)
|
|
if err != nil {
|
|
return MediaInfo{}, err
|
|
}
|
|
streamer, format, closer, err := openDecoder(path)
|
|
if err != nil {
|
|
return MediaInfo{}, err
|
|
}
|
|
defer closer.Close()
|
|
info := MediaInfo{
|
|
Format: ext,
|
|
Streams: []StreamInfo{{
|
|
Index: 0,
|
|
Codec: extTrim(ext),
|
|
Type: "audio",
|
|
SampleRate: int(format.SampleRate),
|
|
Channels: format.NumChannels,
|
|
}},
|
|
}
|
|
if st, err := os.Stat(path); err == nil {
|
|
info.BitRate = st.Size() * 8
|
|
}
|
|
_ = streamer
|
|
return info, nil
|
|
}
|
|
|
|
func extTrim(ext string) string {
|
|
if len(ext) > 0 && ext[0] == '.' {
|
|
return ext[1:]
|
|
}
|
|
return ext
|
|
}
|
|
|
|
// MediaInfo describes decoded input (for optional diagnostics).
|
|
type MediaInfo struct {
|
|
Format string `json:"format"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
BitRate int64 `json:"bit_rate"`
|
|
Streams []StreamInfo `json:"streams"`
|
|
}
|
|
|
|
type StreamInfo struct {
|
|
Index int `json:"index"`
|
|
Codec string `json:"codec"`
|
|
Type string `json:"type"`
|
|
SampleRate int `json:"sample_rate,omitempty"`
|
|
Channels int `json:"channels,omitempty"`
|
|
}
|