chore: add Apache License 2.0 and ignore build directory

This commit is contained in:
2026-07-16 17:24:50 +03:00
parent 45e4bce2a0
commit 7affa0dbc8
23 changed files with 1814 additions and 157 deletions
+58 -51
View File
@@ -1,120 +1,127 @@
// Package captions turns audio packets into fixed or speech-aware captions.
// Package captions turns audio packets into presentation-neutral caption events.
package captions
import (
"fmt"
"io"
"strings"
"tea.chunkbyte.com/kato/captioneer/src/audio"
"tea.chunkbyte.com/kato/captioneer/src/config"
"tea.chunkbyte.com/kato/captioneer/src/models"
)
type Processor struct {
settings config.Settings
resources *models.Resources
output io.Writer
settings config.Settings
transcriber Transcriber
emit func(Event) error
fixedSamples []float32
previewSamples []float32
previewActive bool
previewVisible bool
previewStart int
nextPreviewAt int
totalSamples int
}
func New(settings config.Settings, resources *models.Resources, output io.Writer) *Processor {
func New(settings config.Settings, transcriber Transcriber, emit func(Event) error) *Processor {
return &Processor{
settings: settings,
resources: resources,
output: output,
transcriber: transcriber,
emit: emit,
nextPreviewAt: audio.DurationSamples(settings.ChunkDuration),
}
}
func (p *Processor) Accept(samples []float32) {
func (p *Processor) Accept(samples []float32) error {
if len(samples) == 0 {
return
return nil
}
if p.settings.Mode == config.ModeFixed {
p.acceptFixed(samples)
return
return p.acceptFixed(samples)
}
p.acceptVAD(samples)
return p.acceptVAD(samples)
}
func (p *Processor) Flush() {
func (p *Processor) Flush() error {
if p.settings.Mode == config.ModeFixed {
if len(p.fixedSamples) > 0 {
p.emitCommitted(p.totalSamples, p.totalSamples+len(p.fixedSamples), p.fixedSamples)
return p.emitFinal(p.totalSamples, p.totalSamples+len(p.fixedSamples), p.fixedSamples)
}
return
return nil
}
p.resources.FlushVAD()
p.drainVAD()
p.clearPreview()
p.transcriber.FlushVAD()
return p.drainVAD()
}
func (p *Processor) acceptFixed(samples []float32) {
func (p *Processor) acceptFixed(samples []float32) error {
p.fixedSamples = append(p.fixedSamples, samples...)
chunkSize := audio.DurationSamples(p.settings.ChunkDuration)
for len(p.fixedSamples) >= chunkSize {
p.emitCommitted(p.totalSamples, p.totalSamples+chunkSize, p.fixedSamples[:chunkSize])
if err := p.emitFinal(p.totalSamples, p.totalSamples+chunkSize, p.fixedSamples[:chunkSize]); err != nil {
return err
}
p.fixedSamples = p.fixedSamples[chunkSize:]
p.totalSamples += chunkSize
}
return nil
}
func (p *Processor) acceptVAD(samples []float32) {
func (p *Processor) acceptVAD(samples []float32) error {
p.totalSamples += len(samples)
p.resources.AcceptVAD(samples)
p.transcriber.AcceptVAD(samples)
if p.previewActive || audio.RMSDBFS(samples) >= p.settings.PreviewThresholdDB {
p.previewActive = true
if !p.previewActive {
p.previewActive = true
p.previewStart = p.totalSamples - len(samples)
}
p.previewSamples = append(p.previewSamples, samples...)
if len(p.previewSamples) >= p.nextPreviewAt {
p.emitPreview(p.previewSamples)
if err := p.emitProvisional(); err != nil {
return err
}
p.nextPreviewAt += audio.DurationSamples(p.settings.ChunkDuration)
}
}
p.drainVAD()
return p.drainVAD()
}
func (p *Processor) drainVAD() {
func (p *Processor) drainVAD() error {
for {
segment, ok := p.resources.NextSpeechSegment()
start, samples, ok := p.transcriber.NextSpeechSegment()
if !ok {
return
return nil
}
if err := p.emitFinal(start, start+len(samples), samples); err != nil {
return err
}
p.clearPreview()
p.emitCommitted(segment.Start, segment.Start+len(segment.Samples), segment.Samples)
p.previewSamples = nil
p.previewActive = false
p.previewStart = 0
p.nextPreviewAt = audio.DurationSamples(p.settings.ChunkDuration)
}
}
func (p *Processor) emitPreview(samples []float32) {
text := p.resources.Decode(samples)
func (p *Processor) emitProvisional() error {
text := strings.TrimSpace(p.transcriber.Decode(p.previewSamples))
if text == "" {
return
return nil
}
fmt.Fprintf(p.output, "\r\033[2K… %s", text)
p.previewVisible = true
return p.emit(Event{
Kind: Provisional,
Text: text,
StartedAt: audio.SamplesDuration(p.previewStart),
EndedAt: audio.SamplesDuration(p.previewStart + len(p.previewSamples)),
})
}
func (p *Processor) emitCommitted(start, end int, samples []float32) {
text := p.resources.Decode(samples)
func (p *Processor) emitFinal(start, end int, samples []float32) error {
text := strings.TrimSpace(p.transcriber.Decode(samples))
if text == "" {
return
}
p.clearPreview()
fmt.Fprintf(p.output, "[%s-%s] %s\n", audio.FormatTime(start), audio.FormatTime(end), text)
}
func (p *Processor) clearPreview() {
if p.previewVisible {
fmt.Fprint(p.output, "\r\033[2K")
p.previewVisible = false
return p.emit(Event{Kind: Hide})
}
return p.emit(Event{
Kind: Final,
Text: text,
StartedAt: audio.SamplesDuration(start),
EndedAt: audio.SamplesDuration(end),
})
}