3525e6b8fb
Go orchestrator: spawns audiotee for capture (system audio + mic, two tracks), segments with an energy-threshold VAD, transcribes each segment via qwen-asr (subprocess per segment, vendored as a pinned git submodule in third_party/qwen-asr), and serves the live transcript over SSE while also writing it durably to a text file. Includes a glossary/prompt-leakage guard (internal/asr/leak.go) that discards a segment if the model echoes the biasing prompt instead of transcribing. cmd/transcriptor-ai is the main orchestrator; cmd/transcript-tail is a minimal terminal SSE client. See CLAUDE.md for the full architecture and the reasoning behind each choice (Go over Python/Swift/Rust, SSE over WebSocket, why the VAD is a hand-rolled heuristic, etc). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
50 lines
1.5 KiB
Go
50 lines
1.5 KiB
Go
package transcript
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
// FileWriter appends each Message to a text file as a human-readable line,
|
|
// flushed to disk immediately. Unlike Broadcaster.Publish (which drops
|
|
// messages for a slow/stalled SSE client on purpose), FileWriter never
|
|
// drops — this is the durable record a crash shouldn't lose data from, so
|
|
// every Write is synchronous and fsync'd before returning.
|
|
type FileWriter struct {
|
|
mu sync.Mutex
|
|
f *os.File
|
|
}
|
|
|
|
// NewFileWriter opens path for appending, creating it if needed. Appending
|
|
// (not truncating) means restarting transcriptor-ai against the same path
|
|
// continues the same transcript file rather than discarding it.
|
|
func NewFileWriter(path string) (*FileWriter, error) {
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open transcript file: %w", err)
|
|
}
|
|
return &FileWriter{f: f}, nil
|
|
}
|
|
|
|
// Write appends one line for msg and fsyncs before returning.
|
|
func (fw *FileWriter) Write(msg Message) error {
|
|
fw.mu.Lock()
|
|
defer fw.mu.Unlock()
|
|
line := fmt.Sprintf("[%s] [%s] %s\n", msg.Timestamp.Format("15:04:05"), msg.Track, msg.Text)
|
|
if _, err := io.WriteString(fw.f, line); err != nil {
|
|
return fmt.Errorf("write transcript line: %w", err)
|
|
}
|
|
if err := fw.f.Sync(); err != nil {
|
|
return fmt.Errorf("sync transcript file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fw *FileWriter) Close() error {
|
|
fw.mu.Lock()
|
|
defer fw.mu.Unlock()
|
|
return fw.f.Close()
|
|
}
|