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,66 @@
|
||||
// Command transcript-tail is a minimal terminal client for transcriptor-ai's
|
||||
// live SSE transcript endpoint — connects, parses each `data: {...}` event,
|
||||
// and prints just the text, instead of raw JSON (`curl -N` works too, but
|
||||
// isn't pleasant to read live). See CLAUDE.md for the SSE message format.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/stephanetailland/transcriptor-ai/internal/transcript"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "transcript-tail:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
url := flag.String("url", "http://localhost:8420/events", "transcriptor-ai SSE endpoint to follow")
|
||||
flag.Parse()
|
||||
|
||||
resp, err := http.Get(*url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
data, ok := strings.CutPrefix(scanner.Text(), "data: ")
|
||||
if !ok {
|
||||
continue // SSE keep-alive/blank lines, comments, etc.
|
||||
}
|
||||
var msg transcript.Message
|
||||
if err := json.Unmarshal([]byte(data), &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
printMessage(msg)
|
||||
}
|
||||
// The connection ending here — whether cleanly or as a network-level
|
||||
// error — almost always just means the transcriptor-ai server stopped.
|
||||
// We can't reliably tell that apart from a real connection problem, so
|
||||
// don't be alarmist about it: this isn't a failure of transcript-tail
|
||||
// itself, the way a startup connection error (above) is.
|
||||
fmt.Fprintln(os.Stderr, "transcript-tail: stream ended (server stopped?)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func printMessage(msg transcript.Message) {
|
||||
marker := " "
|
||||
if !msg.IsFinal {
|
||||
marker = "…" // still a partial hypothesis, may be revised
|
||||
}
|
||||
fmt.Printf("[%s%s] %s\n", msg.Track, marker, msg.Text)
|
||||
}
|
||||
Reference in New Issue
Block a user