Files
Captioneer/src/output/transcript/transcript.go
T

103 lines
2.2 KiB
Go
Raw Normal View History

// Package transcript writes finalized captions to timestamped session logs.
package transcript
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"tea.chunkbyte.com/kato/captioneer/src/captions"
)
type Sink struct {
mu sync.Mutex
file *os.File
writer *bufio.Writer
now func() time.Time
closed bool
path string
}
func New(directory string) (*Sink, error) {
return newWithClock(directory, time.Now)
}
func newWithClock(directory string, now func() time.Time) (*Sink, error) {
if strings.TrimSpace(directory) == "" {
return nil, errors.New("transcript directory cannot be empty")
}
if err := os.MkdirAll(directory, 0o700); err != nil {
return nil, fmt.Errorf("create transcript directory: %w", err)
}
startedAt := now()
base := "captioneer-" + startedAt.Format("2006-01-02_15-04-05.000")
var file *os.File
var path string
for suffix := 0; suffix < 1000; suffix++ {
name := base + ".log"
if suffix > 0 {
name = fmt.Sprintf("%s-%d.log", base, suffix)
}
path = filepath.Join(directory, name)
candidate, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if errors.Is(err, os.ErrExist) {
continue
}
if err != nil {
return nil, fmt.Errorf("create transcript: %w", err)
}
file = candidate
break
}
if file == nil {
return nil, errors.New("create transcript: too many files share this timestamp")
}
return &Sink{file: file, writer: bufio.NewWriter(file), now: now, path: path}, nil
}
func (s *Sink) Path() string {
return s.path
}
func (s *Sink) Publish(event captions.Event) error {
if event.Kind != captions.Final {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return errors.New("transcript is closed")
}
text := strings.Join(strings.Fields(event.Text), " ")
if text == "" {
return nil
}
_, err := fmt.Fprintf(
s.writer,
"%s [%.2fs-%.2fs] %s\n",
s.now().Format(time.RFC3339Nano),
event.StartedAt.Seconds(),
event.EndedAt.Seconds(),
text,
)
if err != nil {
return err
}
return s.writer.Flush()
}
func (s *Sink) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
s.closed = true
return errors.Join(s.writer.Flush(), s.file.Close())
}