165 lines
3.1 KiB
Go
165 lines
3.1 KiB
Go
// Package history retains a bounded in-memory caption transcript for the GUI.
|
|
package history
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"tea.chunkbyte.com/kato/captioneer/src/captions"
|
|
)
|
|
|
|
type Entry struct {
|
|
CreatedAt time.Time
|
|
Text string
|
|
StartedAt time.Duration
|
|
EndedAt time.Duration
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Finals []Entry
|
|
Provisional *Entry
|
|
}
|
|
|
|
type Sink struct {
|
|
mu sync.Mutex
|
|
capacity int
|
|
enabled bool
|
|
finals []Entry
|
|
provisional *Entry
|
|
subscribers map[chan Snapshot]struct{}
|
|
closed bool
|
|
}
|
|
|
|
func New(capacity int, enabled bool) *Sink {
|
|
if capacity < 1 {
|
|
capacity = 1
|
|
}
|
|
return &Sink{
|
|
capacity: capacity,
|
|
enabled: enabled,
|
|
subscribers: make(map[chan Snapshot]struct{}),
|
|
}
|
|
}
|
|
|
|
func (s *Sink) Publish(event captions.Event) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return errors.New("caption history is closed")
|
|
}
|
|
if !s.enabled {
|
|
return nil
|
|
}
|
|
now := time.Now()
|
|
switch event.Kind {
|
|
case captions.Provisional:
|
|
s.provisional = &Entry{CreatedAt: now, Text: event.Text, StartedAt: event.StartedAt, EndedAt: event.EndedAt}
|
|
case captions.Final:
|
|
s.provisional = nil
|
|
s.finals = append(s.finals, Entry{CreatedAt: now, Text: event.Text, StartedAt: event.StartedAt, EndedAt: event.EndedAt})
|
|
if len(s.finals) > s.capacity {
|
|
s.finals = append([]Entry(nil), s.finals[len(s.finals)-s.capacity:]...)
|
|
}
|
|
case captions.Hide:
|
|
s.provisional = nil
|
|
}
|
|
s.publishLocked()
|
|
return nil
|
|
}
|
|
|
|
func (s *Sink) SetEnabled(enabled bool) {
|
|
s.mu.Lock()
|
|
s.enabled = enabled
|
|
if !enabled {
|
|
s.provisional = nil
|
|
}
|
|
s.publishLocked()
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *Sink) Clear() {
|
|
s.mu.Lock()
|
|
s.finals = nil
|
|
s.provisional = nil
|
|
s.publishLocked()
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *Sink) Snapshot() Snapshot {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.snapshotLocked()
|
|
}
|
|
|
|
func (s *Sink) Transcript() string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
lines := make([]string, len(s.finals))
|
|
for index, entry := range s.finals {
|
|
lines[index] = entry.Text
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (s *Sink) Subscribe(buffer int) (<-chan Snapshot, func()) {
|
|
if buffer < 1 {
|
|
buffer = 1
|
|
}
|
|
updates := make(chan Snapshot, buffer)
|
|
s.mu.Lock()
|
|
s.subscribers[updates] = struct{}{}
|
|
updates <- s.snapshotLocked()
|
|
s.mu.Unlock()
|
|
return updates, func() {
|
|
s.mu.Lock()
|
|
if _, ok := s.subscribers[updates]; ok {
|
|
delete(s.subscribers, updates)
|
|
close(updates)
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (s *Sink) Close() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return nil
|
|
}
|
|
s.closed = true
|
|
for subscriber := range s.subscribers {
|
|
close(subscriber)
|
|
}
|
|
s.subscribers = nil
|
|
return nil
|
|
}
|
|
|
|
func (s *Sink) snapshotLocked() Snapshot {
|
|
snapshot := Snapshot{Finals: append([]Entry(nil), s.finals...)}
|
|
if s.provisional != nil {
|
|
copy := *s.provisional
|
|
snapshot.Provisional = ©
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func (s *Sink) publishLocked() {
|
|
snapshot := s.snapshotLocked()
|
|
for subscriber := range s.subscribers {
|
|
select {
|
|
case subscriber <- snapshot:
|
|
default:
|
|
select {
|
|
case <-subscriber:
|
|
default:
|
|
}
|
|
select {
|
|
case subscriber <- snapshot:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|