refactor: extract flag parsing, validation, and packet processing into functions; add README and .gitignore

This commit is contained in:
2026-07-16 11:56:40 +03:00
parent f473846bf6
commit d042637351
13 changed files with 585 additions and 8358 deletions
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"fmt"
"io"
"math"
"strings"
"time"
sherpa "github.com/k2-fsa/sherpa-onnx-go-linux"
)
type captionProcessor struct {
cfg settings
recognizer *sherpa.OfflineRecognizer
vad *sherpa.VoiceActivityDetector
output io.Writer
fixedSamples []float32
previewSamples []float32
previewActive bool
previewVisible bool
nextPreviewAt int
totalSamples int
}
func newCaptionProcessor(cfg settings, recognizer *sherpa.OfflineRecognizer, vad *sherpa.VoiceActivityDetector, output io.Writer) *captionProcessor {
return &captionProcessor{
cfg: cfg,
recognizer: recognizer,
vad: vad,
output: output,
nextPreviewAt: durationSamples(cfg.chunkDuration),
}
}
func (p *captionProcessor) accept(samples []float32) {
if len(samples) == 0 {
return
}
if p.cfg.mode == "fixed" {
p.acceptFixed(samples)
return
}
p.acceptVAD(samples)
}
func (p *captionProcessor) acceptFixed(samples []float32) {
p.fixedSamples = append(p.fixedSamples, samples...)
chunkSize := durationSamples(p.cfg.chunkDuration)
for len(p.fixedSamples) >= chunkSize {
p.emitCommitted(p.totalSamples, p.totalSamples+chunkSize, p.fixedSamples[:chunkSize])
p.fixedSamples = p.fixedSamples[chunkSize:]
p.totalSamples += chunkSize
}
}
func (p *captionProcessor) acceptVAD(samples []float32) {
p.totalSamples += len(samples)
p.vad.AcceptWaveform(samples)
if p.previewActive || rmsDBFS(samples) >= p.cfg.previewThresholdDB {
p.previewActive = true
p.previewSamples = append(p.previewSamples, samples...)
if len(p.previewSamples) >= p.nextPreviewAt {
p.emitPreview(p.previewSamples)
p.nextPreviewAt += durationSamples(p.cfg.chunkDuration)
}
}
p.drainVAD()
}
func (p *captionProcessor) drainVAD() {
for !p.vad.IsEmpty() {
segment := p.vad.Front()
p.vad.Pop()
p.clearPreview()
p.emitCommitted(segment.Start, segment.Start+len(segment.Samples), segment.Samples)
p.previewSamples = nil
p.previewActive = false
p.nextPreviewAt = durationSamples(p.cfg.chunkDuration)
}
}
func (p *captionProcessor) flush() {
if p.cfg.mode == "fixed" {
if len(p.fixedSamples) > 0 {
p.emitCommitted(p.totalSamples, p.totalSamples+len(p.fixedSamples), p.fixedSamples)
}
return
}
p.vad.Flush()
p.drainVAD()
p.clearPreview()
}
func (p *captionProcessor) emitPreview(samples []float32) {
text := p.decode(samples)
if text == "" {
return
}
fmt.Fprintf(p.output, "\r\033[2K… %s", text)
p.previewVisible = true
}
func (p *captionProcessor) emitCommitted(start, end int, samples []float32) {
text := p.decode(samples)
if text == "" {
return
}
p.clearPreview()
fmt.Fprintf(p.output, "[%s-%s] %s\n", formatAudioTime(start), formatAudioTime(end), text)
}
func (p *captionProcessor) clearPreview() {
if p.previewVisible {
fmt.Fprint(p.output, "\r\033[2K")
p.previewVisible = false
}
}
func (p *captionProcessor) decode(samples []float32) string {
if len(samples) == 0 {
return ""
}
stream := sherpa.NewOfflineStream(p.recognizer)
defer sherpa.DeleteOfflineStream(stream)
stream.AcceptWaveform(sampleRate, samples)
p.recognizer.Decode(stream)
return strings.TrimSpace(stream.GetResult().Text)
}
func durationSamples(duration time.Duration) int {
return int(duration * sampleRate / time.Second)
}
func rmsDBFS(samples []float32) float64 {
if len(samples) == 0 {
return math.Inf(-1)
}
var sum float64
for _, sample := range samples {
sum += float64(sample * sample)
}
rms := math.Sqrt(sum / float64(len(samples)))
if rms == 0 {
return math.Inf(-1)
}
return 20 * math.Log10(rms)
}
func formatAudioTime(samples int) string {
return fmt.Sprintf("%.2fs", float64(samples)/sampleRate)
}