initial commit: live meeting transcription pipeline
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>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user