Files
sttlab-tech 3525e6b8fb 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>
2026-08-09 13:18:57 +02:00

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()
}