docs: update README with detailed desktop features and setup requirements
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// 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())
|
||||
}
|
||||
Reference in New Issue
Block a user