refactor: extract flag parsing, validation, and packet processing into functions; add README and .gitignore
This commit is contained in:
+154
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user