first commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
type API struct {
|
||||
Addr string `yaml:"addr"`
|
||||
ModelsDir string `yaml:"models_dir"`
|
||||
CacheDir string `yaml:"cache_dir"`
|
||||
// DefaultModel: whisper model id for OpenAI /v1/audio/transcriptions (maps whisper-1).
|
||||
DefaultModel string `yaml:"default_model"`
|
||||
Language string `yaml:"language"`
|
||||
Transcript Transcript `yaml:"transcript"`
|
||||
DefaultSpeakers bool `yaml:"default_speakers"`
|
||||
Prompt string `yaml:"prompt"`
|
||||
Threads uint `yaml:"threads"`
|
||||
MaxContext uint `yaml:"max_context"`
|
||||
BeamSize uint `yaml:"beam_size"`
|
||||
EntropyThold float64 `yaml:"entropy_thold"`
|
||||
VAD VAD `yaml:"vad"`
|
||||
Debug bool `yaml:"debug"`
|
||||
SpeedUp bool `yaml:"speedup"`
|
||||
Translate bool `yaml:"translate"`
|
||||
DefaultPunctuation bool `yaml:"default_punctuation"`
|
||||
// DefaultAsync: STT via API enqueues to cache/waiting and returns taskID (use ?async=0 for sync).
|
||||
DefaultAsync bool `yaml:"default_async"`
|
||||
// Garbage: artifact substrings removed from transcript text and words (default includes *выбая*).
|
||||
Garbage []string `yaml:"garbage"`
|
||||
}
|
||||
|
||||
func (a API) WithDefaults() API {
|
||||
a.VAD = a.VAD.WithDefaults()
|
||||
a.Transcript = a.Transcript.WithDefaults()
|
||||
if strings.TrimSpace(a.Language) == "" {
|
||||
a.Language = "ru"
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// GarbagePatterns returns garbage filter list (never empty unless explicitly set to [] in YAML).
|
||||
func (a API) GarbagePatterns() []string {
|
||||
return a.garbagePatterns()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Diarization struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ModelDir string `yaml:"model_dir"`
|
||||
SegmentationModel string `yaml:"segmentation_model"`
|
||||
EmbeddingModel string `yaml:"embedding_model"`
|
||||
NumThreads int `yaml:"num_threads"`
|
||||
NumClusters int `yaml:"num_clusters"`
|
||||
ClusteringThreshold float32 `yaml:"clustering_threshold"`
|
||||
MinDurationOn float32 `yaml:"min_duration_on"`
|
||||
MinDurationOff float32 `yaml:"min_duration_off"`
|
||||
}
|
||||
|
||||
func (d Diarization) WithDefaults() Diarization {
|
||||
if d.ModelDir == "" {
|
||||
d.ModelDir = "./models/diarization"
|
||||
}
|
||||
if d.SegmentationModel == "" {
|
||||
d.SegmentationModel = "pyannote-segmentation-3-0/model.onnx"
|
||||
}
|
||||
if d.EmbeddingModel == "" {
|
||||
d.EmbeddingModel = "3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx"
|
||||
}
|
||||
if d.NumThreads <= 0 {
|
||||
d.NumThreads = 2
|
||||
}
|
||||
if d.ClusteringThreshold <= 0 {
|
||||
d.ClusteringThreshold = 0.5
|
||||
}
|
||||
if d.MinDurationOn <= 0 {
|
||||
d.MinDurationOn = 0.3
|
||||
}
|
||||
if d.MinDurationOff <= 0 {
|
||||
d.MinDurationOff = 0.5
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d Diarization) SegmentationPath() string {
|
||||
return resolveModelPath(d.ModelDir, d.SegmentationModel)
|
||||
}
|
||||
|
||||
func (d Diarization) EmbeddingPath() string {
|
||||
return resolveModelPath(d.ModelDir, d.EmbeddingModel)
|
||||
}
|
||||
|
||||
func (d Diarization) ModelsPresent() bool {
|
||||
if _, err := os.Stat(d.SegmentationPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(d.EmbeddingPath()); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func resolveModelPath(dir, name string) string {
|
||||
if filepath.IsAbs(name) {
|
||||
return name
|
||||
}
|
||||
return filepath.Join(dir, name)
|
||||
}
|
||||
|
||||
func (d Diarization) Active() bool {
|
||||
return d.Enabled
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
API API `yaml:"api"`
|
||||
Transcode Transcode `yaml:"transcode"`
|
||||
Punctuation Punctuation `yaml:"punctuation"`
|
||||
Diarization Diarization `yaml:"diarization"`
|
||||
}
|
||||
|
||||
func DefaultFile() File {
|
||||
return File{
|
||||
API: API{
|
||||
Addr: ":8080",
|
||||
ModelsDir: "./models",
|
||||
CacheDir: "./cache",
|
||||
Threads: uint(runtime.NumCPU()),
|
||||
Language: "ru",
|
||||
Transcript: Transcript{}.WithDefaults(),
|
||||
MaxContext: 32,
|
||||
BeamSize: 5,
|
||||
EntropyThold: 2.4,
|
||||
DefaultAsync: true,
|
||||
Garbage: DefaultGarbage(),
|
||||
},
|
||||
Diarization: Diarization{}.WithDefaults(),
|
||||
Transcode: Transcode{}.WithDefaults(),
|
||||
Punctuation: Punctuation{Enabled: false, Engine: "off"}.WithDefaults(),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFile(path string) (File, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
cfg := DefaultFile()
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return File{}, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (f File) APIConfig() API {
|
||||
return f.API
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
err := os.WriteFile(path, []byte(`
|
||||
api:
|
||||
addr: ":9090"
|
||||
models_dir: "/data/models"
|
||||
language: ru
|
||||
`), 0o644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.API.Addr != ":9090" {
|
||||
t.Fatalf("addr: got %q", cfg.API.Addr)
|
||||
}
|
||||
if cfg.API.ModelsDir != "/data/models" {
|
||||
t.Fatalf("models_dir: got %q", cfg.API.ModelsDir)
|
||||
}
|
||||
if cfg.API.Language != "ru" {
|
||||
t.Fatalf("language: got %q", cfg.API.Language)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
// DefaultGarbage returns STT artifact tokens filtered from API/CLI transcript output.
|
||||
func DefaultGarbage() []string {
|
||||
return []string{"*выбая*"}
|
||||
}
|
||||
|
||||
func (a API) garbagePatterns() []string {
|
||||
if a.Garbage == nil {
|
||||
return DefaultGarbage()
|
||||
}
|
||||
return a.Garbage
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultGarbage(t *testing.T) {
|
||||
if len(DefaultGarbage()) == 0 || DefaultGarbage()[0] != "*выбая*" {
|
||||
t.Fatalf("got %v", DefaultGarbage())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPI_GarbagePatterns_default(t *testing.T) {
|
||||
p := (API{}).GarbagePatterns()
|
||||
if len(p) != 1 || p[0] != "*выбая*" {
|
||||
t.Fatalf("got %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPI_GarbagePatterns_explicitEmpty(t *testing.T) {
|
||||
p := (API{Garbage: []string{}}).GarbagePatterns()
|
||||
if len(p) != 0 {
|
||||
t.Fatalf("got %v", p)
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func LoadResolved(path string) (File, error) {
|
||||
if path == "" {
|
||||
if _, err := os.Stat("config.yaml"); err == nil {
|
||||
path = "config.yaml"
|
||||
} else {
|
||||
return DefaultFile(), nil
|
||||
}
|
||||
}
|
||||
return LoadFile(path)
|
||||
}
|
||||
|
||||
func mergeVAD(c *cli.Context, v VAD) VAD {
|
||||
if c.IsSet("vad") {
|
||||
v.Enabled = c.Bool("vad")
|
||||
}
|
||||
if c.IsSet("vad-model") {
|
||||
v.Model = c.String("vad-model")
|
||||
}
|
||||
if c.IsSet("vad-threshold") {
|
||||
v.Threshold = c.Float64("vad-threshold")
|
||||
}
|
||||
if c.IsSet("vad-min-speech-ms") {
|
||||
v.MinSpeechMs = c.Int("vad-min-speech-ms")
|
||||
}
|
||||
if c.IsSet("vad-min-silence-ms") {
|
||||
v.MinSilenceMs = c.Int("vad-min-silence-ms")
|
||||
}
|
||||
if c.IsSet("vad-max-speech-sec") {
|
||||
v.MaxSpeechSec = c.Float64("vad-max-speech-sec")
|
||||
}
|
||||
if c.IsSet("vad-speech-pad-ms") {
|
||||
v.SpeechPadMs = c.Int("vad-speech-pad-ms")
|
||||
}
|
||||
if c.IsSet("vad-samples-overlap") {
|
||||
v.SamplesOverlap = c.Float64("vad-samples-overlap")
|
||||
}
|
||||
return v.WithDefaults()
|
||||
}
|
||||
|
||||
func mergeAPI(c *cli.Context, a API) API {
|
||||
if c.IsSet("addr") {
|
||||
a.Addr = c.String("addr")
|
||||
}
|
||||
if c.IsSet("models-dir") {
|
||||
a.ModelsDir = c.String("models-dir")
|
||||
}
|
||||
if c.IsSet("cache-dir") {
|
||||
a.CacheDir = c.String("cache-dir")
|
||||
}
|
||||
if c.IsSet("threads") {
|
||||
a.Threads = c.Uint("threads")
|
||||
}
|
||||
if c.IsSet("language") {
|
||||
a.Language = c.String("language")
|
||||
}
|
||||
if c.IsSet("debug") {
|
||||
a.Debug = c.Bool("debug")
|
||||
}
|
||||
if c.IsSet("speedup") {
|
||||
a.SpeedUp = c.Bool("speedup")
|
||||
}
|
||||
if c.IsSet("translate") {
|
||||
a.Translate = c.Bool("translate")
|
||||
}
|
||||
if c.IsSet("prompt") {
|
||||
a.Prompt = c.String("prompt")
|
||||
}
|
||||
if c.IsSet("max-context") {
|
||||
a.MaxContext = c.Uint("max-context")
|
||||
}
|
||||
if c.IsSet("beam-size") {
|
||||
a.BeamSize = c.Uint("beam-size")
|
||||
}
|
||||
if c.IsSet("entropy-thold") {
|
||||
a.EntropyThold = c.Float64("entropy-thold")
|
||||
}
|
||||
a.VAD = mergeVAD(c, a.VAD)
|
||||
if c.IsSet("default-punctuation") {
|
||||
a.DefaultPunctuation = c.Bool("default-punctuation")
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func APIFromCLI(c *cli.Context) (API, error) {
|
||||
file, err := LoadResolved(c.String("config"))
|
||||
if err != nil {
|
||||
return API{}, err
|
||||
}
|
||||
return mergeAPI(c, file.API), nil
|
||||
}
|
||||
|
||||
func TranscodeFromCLI(c *cli.Context) (Transcode, error) {
|
||||
file, err := LoadResolved(c.String("config"))
|
||||
if err != nil {
|
||||
return Transcode{}, err
|
||||
}
|
||||
return file.Transcode.WithDefaults(), nil
|
||||
}
|
||||
|
||||
func mergePunctuation(c *cli.Context, p Punctuation) Punctuation {
|
||||
if c.IsSet("punctuation-enabled") {
|
||||
p.Enabled = c.Bool("punctuation-enabled")
|
||||
}
|
||||
if c.IsSet("punctuation-engine") {
|
||||
p.Engine = c.String("punctuation-engine")
|
||||
}
|
||||
if c.IsSet("punctuation-default-on") {
|
||||
p.DefaultOn = c.Bool("punctuation-default-on")
|
||||
}
|
||||
return p.WithDefaults()
|
||||
}
|
||||
|
||||
func PunctuationFromCLI(c *cli.Context) (Punctuation, error) {
|
||||
file, err := LoadResolved(c.String("config"))
|
||||
if err != nil {
|
||||
return Punctuation{}, err
|
||||
}
|
||||
return mergePunctuation(c, file.Punctuation), nil
|
||||
}
|
||||
|
||||
func DiarizationFromCLI(c *cli.Context) (Diarization, error) {
|
||||
file, err := LoadResolved(c.String("config"))
|
||||
if err != nil {
|
||||
return Diarization{}, err
|
||||
}
|
||||
return file.Diarization.WithDefaults(), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultFile_apiDefaults(t *testing.T) {
|
||||
f := DefaultFile()
|
||||
if f.API.Addr == "" {
|
||||
t.Fatal("expected API listen addr")
|
||||
}
|
||||
if f.API.ModelsDir == "" {
|
||||
t.Fatal("expected models_dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdirTemp_usesTmpRoot(t *testing.T) {
|
||||
dir, err := MkdirTemp("go-whisper-api-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(dir) }()
|
||||
if !strings.HasPrefix(dir, TempRoot+"/") {
|
||||
t.Fatalf("temp dir should be under %s, got %s", TempRoot, dir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (p Punctuation) XLMJoinSentences() bool {
|
||||
return p.ApplySBD
|
||||
}
|
||||
|
||||
type Punctuation struct {
|
||||
Command []string `yaml:"command"`
|
||||
NumThreads int `yaml:"num_threads"`
|
||||
TimeoutSec int `yaml:"timeout_sec"`
|
||||
Engine string `yaml:"engine"`
|
||||
ModelDir string `yaml:"model_dir"`
|
||||
ModelFile string `yaml:"model_file"`
|
||||
SPModel string `yaml:"sp_model"`
|
||||
ConfigFile string `yaml:"config_file"`
|
||||
BpeVocab string `yaml:"bpe_vocab"`
|
||||
HTTPURL string `yaml:"http_url"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
DefaultOn bool `yaml:"default_on"`
|
||||
ApplySBD bool `yaml:"apply_sbd"`
|
||||
}
|
||||
|
||||
func (p Punctuation) WithDefaults() Punctuation {
|
||||
if p.Engine == "" {
|
||||
p.Engine = "heuristic"
|
||||
}
|
||||
engine := strings.ToLower(strings.TrimSpace(p.Engine))
|
||||
if p.ModelDir == "" {
|
||||
if engine == "xlm" || engine == "xlm-roberta" || engine == "roberta" {
|
||||
p.ModelDir = "./models/punctuation/xlm-roberta"
|
||||
} else {
|
||||
p.ModelDir = "./models/punctuation/ct-transformer-zh-en-int8"
|
||||
}
|
||||
}
|
||||
if p.ModelFile == "" {
|
||||
if engine == "xlm" || engine == "xlm-roberta" || engine == "roberta" {
|
||||
p.ModelFile = "model.onnx"
|
||||
} else {
|
||||
p.ModelFile = "model.int8.onnx"
|
||||
}
|
||||
}
|
||||
if p.NumThreads <= 0 {
|
||||
p.NumThreads = 2
|
||||
}
|
||||
if p.TimeoutSec <= 0 {
|
||||
p.TimeoutSec = 120
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p Punctuation) Timeout() time.Duration {
|
||||
p = p.WithDefaults()
|
||||
return time.Duration(p.TimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
func (p Punctuation) Active() bool {
|
||||
p = p.WithDefaults()
|
||||
return p.Enabled && p.Engine != "" && !strings.EqualFold(p.Engine, "off")
|
||||
}
|
||||
|
||||
func (p Punctuation) ModelPath() string {
|
||||
p = p.WithDefaults()
|
||||
return filepath.Join(p.ModelDir, p.ModelFile)
|
||||
}
|
||||
|
||||
func (p Punctuation) SPModelPath() string {
|
||||
p = p.WithDefaults()
|
||||
if p.SPModel == "" {
|
||||
return filepath.Join(p.ModelDir, "sp.model")
|
||||
}
|
||||
return filepath.Join(p.ModelDir, p.SPModel)
|
||||
}
|
||||
|
||||
func (p Punctuation) XLMConfigPath() string {
|
||||
p = p.WithDefaults()
|
||||
if p.ConfigFile == "" {
|
||||
return filepath.Join(p.ModelDir, "config.yaml")
|
||||
}
|
||||
return filepath.Join(p.ModelDir, p.ConfigFile)
|
||||
}
|
||||
|
||||
func (p Punctuation) BpeVocabPath() string {
|
||||
p = p.WithDefaults()
|
||||
if p.BpeVocab == "" {
|
||||
return filepath.Join(p.ModelDir, "bpe.vocab")
|
||||
}
|
||||
return filepath.Join(p.ModelDir, p.BpeVocab)
|
||||
}
|
||||
|
||||
func (p Punctuation) ShouldApplyAPI(r *http.Request, apiDefault bool) bool {
|
||||
if !p.Active() {
|
||||
return false
|
||||
}
|
||||
q := strings.TrimSpace(r.URL.Query().Get("punctuation"))
|
||||
if q != "" {
|
||||
return parsePunctuationQuery(q, true)
|
||||
}
|
||||
return apiDefault || p.Enabled
|
||||
}
|
||||
|
||||
func parsePunctuationQuery(raw string, def bool) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return def
|
||||
}
|
||||
switch strings.ToLower(raw) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
}
|
||||
b, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
)
|
||||
|
||||
func TestPunctuation_Active(t *testing.T) {
|
||||
if (Punctuation{Enabled: false, Engine: "heuristic"}).Active() {
|
||||
t.Fatal("disabled should be inactive")
|
||||
}
|
||||
if (Punctuation{Enabled: true, Engine: "off"}).Active() {
|
||||
t.Fatal("engine off should be inactive")
|
||||
}
|
||||
if !(Punctuation{Enabled: true, Engine: "heuristic"}).Active() {
|
||||
t.Fatal("enabled heuristic should be active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPunctuation_ShouldApplyAPI(t *testing.T) {
|
||||
p := Punctuation{Enabled: true, Engine: "heuristic"}
|
||||
|
||||
req := httptest.NewRequest("GET", "/?punctuation=1", nil)
|
||||
if !p.ShouldApplyAPI(req, false) {
|
||||
t.Fatal("query 1 should enable")
|
||||
}
|
||||
|
||||
req = httptest.NewRequest("GET", "/?punctuation=0", nil)
|
||||
if p.ShouldApplyAPI(req, true) {
|
||||
t.Fatal("query 0 should disable")
|
||||
}
|
||||
|
||||
req = httptest.NewRequest("GET", "/", nil)
|
||||
if !p.ShouldApplyAPI(req, false) {
|
||||
t.Fatal("enabled in config => apply when query omitted")
|
||||
}
|
||||
if !p.ShouldApplyAPI(req, true) {
|
||||
t.Fatal("api default true")
|
||||
}
|
||||
|
||||
off := Punctuation{Enabled: false, Engine: "heuristic"}
|
||||
if off.ShouldApplyAPI(req, true) {
|
||||
t.Fatal("master disabled must never apply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePunctuationQuery(t *testing.T) {
|
||||
if !parsePunctuationQuery("1", false) {
|
||||
t.Fatal("1 => true")
|
||||
}
|
||||
if parsePunctuationQuery("0", true) {
|
||||
t.Fatal("0 => false")
|
||||
}
|
||||
if !parsePunctuationQuery("", true) {
|
||||
t.Fatal("empty => default")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
const TempRoot = "/tmp"
|
||||
|
||||
func MkdirTemp(prefix string) (string, error) {
|
||||
return os.MkdirTemp(TempRoot, prefix)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
// Transcode holds audio normalization settings (pure Go decoders; no external ffmpeg).
|
||||
type Transcode struct {
|
||||
// FFmpegPath is deprecated and ignored; kept for backward-compatible YAML.
|
||||
FFmpegPath string `yaml:"ffmpeg_path,omitempty"`
|
||||
}
|
||||
|
||||
func (t Transcode) WithDefaults() Transcode {
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transcript controls how STT segments are joined into one text field (with embedded newlines).
|
||||
type Transcript struct {
|
||||
PauseGapSec float64 `yaml:"pause_gap_sec"`
|
||||
SpeakerLabel string `yaml:"speaker_label"`
|
||||
}
|
||||
|
||||
func (t Transcript) WithDefaults() Transcript {
|
||||
if t.PauseGapSec <= 0 {
|
||||
t.PauseGapSec = 1.5
|
||||
}
|
||||
if strings.TrimSpace(t.SpeakerLabel) == "" {
|
||||
t.SpeakerLabel = "Спикер"
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t Transcript) PauseGapDuration() time.Duration {
|
||||
return time.Duration(t.PauseGapSec * float64(time.Second))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type VAD struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Model string `yaml:"model"`
|
||||
Threshold float64 `yaml:"threshold"`
|
||||
MinSpeechMs int `yaml:"min_speech_duration_ms"`
|
||||
MinSilenceMs int `yaml:"min_silence_duration_ms"`
|
||||
MaxSpeechSec float64 `yaml:"max_speech_duration_s"`
|
||||
SpeechPadMs int `yaml:"speech_pad_ms"`
|
||||
SamplesOverlap float64 `yaml:"samples_overlap"`
|
||||
}
|
||||
|
||||
func DefaultVAD() VAD {
|
||||
return VAD{
|
||||
Threshold: 0.5,
|
||||
MinSpeechMs: 250,
|
||||
MinSilenceMs: 100,
|
||||
MaxSpeechSec: 0,
|
||||
SpeechPadMs: 30,
|
||||
SamplesOverlap: 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
func (v VAD) WithDefaults() VAD {
|
||||
d := DefaultVAD()
|
||||
if v.Threshold <= 0 {
|
||||
v.Threshold = d.Threshold
|
||||
}
|
||||
if v.MinSpeechMs <= 0 {
|
||||
v.MinSpeechMs = d.MinSpeechMs
|
||||
}
|
||||
if v.MinSilenceMs <= 0 {
|
||||
v.MinSilenceMs = d.MinSilenceMs
|
||||
}
|
||||
if v.SpeechPadMs <= 0 {
|
||||
v.SpeechPadMs = d.SpeechPadMs
|
||||
}
|
||||
if v.SamplesOverlap <= 0 {
|
||||
v.SamplesOverlap = d.SamplesOverlap
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (v VAD) ResolveModelPath(modelsDir string) string {
|
||||
if v.Model == "" {
|
||||
return ""
|
||||
}
|
||||
if filepath.IsAbs(v.Model) {
|
||||
return v.Model
|
||||
}
|
||||
if modelsDir == "" {
|
||||
return v.Model
|
||||
}
|
||||
direct := filepath.Join(modelsDir, v.Model)
|
||||
if _, err := os.Stat(direct); err == nil {
|
||||
return direct
|
||||
}
|
||||
// VAD weights often live under models_dir/vad/ (not listed in /spr/models).
|
||||
base := filepath.Base(v.Model)
|
||||
inVAD := filepath.Join(modelsDir, "vad", base)
|
||||
if _, err := os.Stat(inVAD); err == nil {
|
||||
return inVAD
|
||||
}
|
||||
return direct
|
||||
}
|
||||
|
||||
func (v VAD) Validate() error {
|
||||
if !v.Enabled {
|
||||
return nil
|
||||
}
|
||||
if v.Model == "" {
|
||||
return fmt.Errorf("vad.model is required when vad.enabled is true")
|
||||
}
|
||||
if _, err := os.Stat(v.Model); err != nil {
|
||||
return fmt.Errorf("vad model %q: %w", v.Model, err)
|
||||
}
|
||||
if v.Threshold < 0 || v.Threshold > 1 {
|
||||
return fmt.Errorf("vad.threshold must be between 0 and 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVAD_Validate_disabled(t *testing.T) {
|
||||
if err := (VAD{}).Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVAD_Validate_requiresModel(t *testing.T) {
|
||||
err := (VAD{Enabled: true}).Validate()
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVAD_Validate_modelExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
model := filepath.Join(dir, "ggml-silero.bin")
|
||||
if err := os.WriteFile(model, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := VAD{Enabled: true, Model: model, Threshold: 0.5}
|
||||
if err := v.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVAD_ResolveModelPath(t *testing.T) {
|
||||
v := VAD{Model: "ggml-silero-v6.2.0.bin"}
|
||||
got := v.ResolveModelPath("/data/models")
|
||||
want := filepath.Join("/data/models", "ggml-silero-v6.2.0.bin")
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVAD_ResolveModelPath_vadSubdir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vadDir := filepath.Join(dir, "vad")
|
||||
if err := os.MkdirAll(vadDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(vadDir, "vad.bin"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := VAD{Model: "vad.bin"}
|
||||
got := v.ResolveModelPath(dir)
|
||||
want := filepath.Join(dir, "vad", "vad.bin")
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVAD_WithDefaults(t *testing.T) {
|
||||
v := VAD{Enabled: true}.WithDefaults()
|
||||
if v.Threshold != 0.5 || v.MinSpeechMs != 250 || v.MinSilenceMs != 100 {
|
||||
t.Fatalf("unexpected defaults: %+v", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Whisper struct {
|
||||
OutputFormat []string `yaml:"output_format"`
|
||||
Model string `yaml:"model"`
|
||||
AudioPath string `yaml:"audio_path"`
|
||||
Language string `yaml:"language"`
|
||||
Prompt string `yaml:"prompt"`
|
||||
OutputFolder string `yaml:"output_folder"`
|
||||
OutputFilename string `yaml:"output_filename"`
|
||||
Threads uint `yaml:"threads"`
|
||||
MaxContext uint `yaml:"max_context"`
|
||||
BeamSize uint `yaml:"beam_size"`
|
||||
EntropyThold float64 `yaml:"entropy_thold"`
|
||||
VAD VAD `yaml:"vad"`
|
||||
Debug bool `yaml:"debug"`
|
||||
SpeedUp bool `yaml:"speedup"`
|
||||
Translate bool `yaml:"translate"`
|
||||
PrintProgress bool `yaml:"print_progress"`
|
||||
PrintSegment bool `yaml:"print_segment"`
|
||||
}
|
||||
|
||||
func (c *Whisper) ValidateModel() error {
|
||||
if c.Model == "" {
|
||||
return fmt.Errorf("model is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Whisper) Validate() error {
|
||||
if err := c.ValidateModel(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.AudioPath == "" {
|
||||
return fmt.Errorf("audio path is required")
|
||||
}
|
||||
if err := c.VAD.WithDefaults().Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Metadata for Salama1429/xlm-roberta_punctuation_fullstop_truecase (ONNX punctuation).
|
||||
# Install into the model directory:
|
||||
# cp config/xlm-roberta-model.yaml models/punctuation/xlm-roberta/config.yaml
|
||||
# or: make install-xlm-punctuation-config
|
||||
|
||||
languages:
|
||||
- af
|
||||
- am
|
||||
- ar
|
||||
- bg
|
||||
- bn
|
||||
- de
|
||||
- el
|
||||
- en
|
||||
- es
|
||||
- et
|
||||
- fa
|
||||
- fi
|
||||
- fr
|
||||
- gu
|
||||
- hi
|
||||
- hr
|
||||
- hu
|
||||
- id
|
||||
- is
|
||||
- it
|
||||
- ja
|
||||
- kk
|
||||
- kn
|
||||
- ko
|
||||
- ky
|
||||
- lt
|
||||
- lv
|
||||
- mk
|
||||
- ml
|
||||
- mr
|
||||
- nl
|
||||
- or
|
||||
- pa
|
||||
- pl
|
||||
- ps
|
||||
- pt
|
||||
- ro
|
||||
- ru
|
||||
- rw
|
||||
- so
|
||||
- sr
|
||||
- sw
|
||||
- ta
|
||||
- te
|
||||
- tr
|
||||
- uk
|
||||
- zh
|
||||
|
||||
max_length: 256
|
||||
|
||||
pre_labels:
|
||||
- "<NULL>"
|
||||
- "¿"
|
||||
|
||||
post_labels:
|
||||
- "<NULL>"
|
||||
- "<ACRONYM>"
|
||||
- "."
|
||||
- ","
|
||||
- "?"
|
||||
- "?"
|
||||
- ","
|
||||
- "。"
|
||||
- "、"
|
||||
- "・"
|
||||
- "।"
|
||||
- "؟"
|
||||
- "،"
|
||||
- ";"
|
||||
- "።"
|
||||
- "፣"
|
||||
- "፧"
|
||||
|
||||
null_token: "<NULL>"
|
||||
acronym_token: "<ACRONYM>"
|
||||
Reference in New Issue
Block a user