Documentation
¶
Overview ¶
Package meeting owns the runtime behind Meeting 2.0 capture.
A meeting is recorded from two sources at once: the microphone carries the local speaker and a host-provided system channel carries everyone else on the call. The two are deliberately never mixed into one stream — mixing would need clock-drift compensation and echo cancellation, while two independent pipelines give the same result plus a free speaker split, and interleave on a shared wall clock afterwards.
The runtime owns the lifecycle of those pipelines and is the single source of truth for what capture is doing right now. Hosts subscribe to snapshots rather than inferring state from the outcome of the last command.
Index ¶
- Constants
- Variables
- func CallApps(configured []string) []string
- func ChunkTranscript(lines []TranscriptLine, budgetTokens int) [][]TranscriptLine
- func EstimateTokens(lines []TranscriptLine) int
- func FormatOffset(ms int64) string
- func MergeDigests(digests []string) (string, error)
- func NormalizeDigestJSON(text string) (string, error)
- func RenderTranscript(lines []TranscriptLine) string
- func RepairTruncatedJSON(text string) (string, bool)
- type Anchor
- type ChannelSnapshot
- type ChannelState
- type Detection
- type EndWatcher
- type MicrophoneUser
- type NotesBullet
- type NotesDocument
- type NotesSection
- type Options
- type Pipeline
- type PipelineFactory
- type Runtime
- func (r *Runtime) ActiveSessionID() int64
- func (r *Runtime) ElapsedMs() (sessionID int64, elapsedMs int64, ok bool)
- func (r *Runtime) NoteSegmentCommitted(sessionID int64)
- func (r *Runtime) NoteSegmentSubmitted(sessionID int64)
- func (r *Runtime) Pause() (Snapshot, error)
- func (r *Runtime) Resume() (Snapshot, error)
- func (r *Runtime) SetEndedHook(hook func(sessionID int64))
- func (r *Runtime) SetPipelineFactory(factory PipelineFactory)
- func (r *Runtime) Snapshot() Snapshot
- func (r *Runtime) SnapshotFor(sessionID int64) (Snapshot, bool)
- func (r *Runtime) Start(ctx context.Context, opts StartOptions) (Snapshot, error)
- func (r *Runtime) Stop(ctx context.Context) (Snapshot, error)
- func (r *Runtime) Subscribe() (<-chan Snapshot, func())
- type Snapshot
- type StartOptions
- type State
- type Template
- type TemplateSection
- type TranscriptLine
- type Watcher
Constants ¶
const ( TemplateDefaultMeeting = "default_meeting" TemplateOneOnOne = "one_on_one" TemplateSales = "sales" TemplateStandup = "standup" )
The templates a meeting can be written up with. They are code rather than seeded rows so a stored write-up always names a template that still exists, and so improving the wording does not need a data migration. Custom templates have a table waiting for them.
const ( ChannelMicrophone = speechkit.CaptureChannelMicrophone ChannelSystem = speechkit.CaptureChannelSystem )
Channels a meeting records from.
const MaxTranscriptLineRunes = 1800
MaxTranscriptLineRunes bounds one transcript line as the models see it. A meeting channel that never pauses used to arrive as one 19,000-character segment (2026-09-03); no local context window holds that in a single request, and the summary batches and the write-up chunker can only split between lines, never inside one. Lines longer than this are cut into parts at sentence boundaries. The value is fixed rather than derived from the model so the same segment always yields the same parts.
const SectionActionItems = "action_items"
SectionActionItems is the slug of the section action items live in. Callers that surface tasks separately look for it by name.
Variables ¶
var ( // ErrMeetingActive is returned when a second meeting is started while one // is still running. Capture devices and the transcription worker are shared, // so meetings are deliberately exclusive. ErrMeetingActive = errors.New("meeting: a meeting is already being recorded") // ErrNoMeeting is returned by commands that need a running meeting. ErrNoMeeting = errors.New("meeting: no meeting is being recorded") // ErrNoChannels is returned when not a single capture channel could be // opened, which makes the meeting pointless rather than degraded. ErrNoChannels = errors.New("meeting: no capture channel could be opened") )
var ErrDigestNotJSON = errors.New("meeting digest is not JSON")
ErrDigestNotJSON is returned when a model answer carries no JSON object or array at all — empty output, or prose without a JSON body.
Functions ¶
func ChunkTranscript ¶
func ChunkTranscript(lines []TranscriptLine, budgetTokens int) [][]TranscriptLine
ChunkTranscript splits a transcript into pieces that fit a model's context.
A budget of zero means the model can hold the whole meeting, which is the case for every cloud model and the reason the write-up normally happens in a single pass. A small local model gets the meeting in parts instead, split on segment boundaries so no utterance is cut in half.
func EstimateTokens ¶
func EstimateTokens(lines []TranscriptLine) int
EstimateTokens approximates how much of a context window a transcript takes. It is deliberately a rough character ratio: the point is to decide between one pass and several, and being a little pessimistic costs nothing while being wrong in the other direction truncates a meeting.
func FormatOffset ¶
FormatOffset renders a point in the meeting as hh:mm:ss.
func MergeDigests ¶
MergeDigests combines the digests of one segment's parts into a single digest: list fields (chronology, topics, decisions, …) are concatenated in part order, any other field keeps the value of the first part that set it. Every input must be a JSON object.
func NormalizeDigestJSON ¶
NormalizeDigestJSON extracts the JSON document from a model answer and returns it compacted. Local models routinely wrap the requested JSON in a fenced code block or prefix it with a sentence; storing that verbatim as a "ready" digest fed fenced text into every rollup and write-up downstream (96 of 99 stored digests were unparsable on 2026-09-03). Empty or non-JSON answers return ErrDigestNotJSON so the batch is retried instead of being marked ready with nothing in it.
func RenderTranscript ¶
func RenderTranscript(lines []TranscriptLine) string
RenderTranscript writes the transcript the way the write-up prompt reads it: one line per segment, each labelled with the segment id a bullet cites, who spoke, and when. The id is what makes a claim in the notes checkable against what was actually said.
func RepairTruncatedJSON ¶
RepairTruncatedJSON salvages a JSON answer a model stopped writing before the closing brackets — the usual result of an answer budget that was too small. It cuts the text back to the last complete value and closes every open array and object, so `{"topics":["a","b","c` becomes `{"topics":["a","b"]}`. The second result is false when nothing complete could be kept or the text is not a truncated JSON document at all.
Types ¶
type ChannelSnapshot ¶
type ChannelSnapshot struct {
Channel string `json:"channel"`
State ChannelState `json:"state"`
Message string `json:"message,omitempty"`
}
ChannelSnapshot is the runtime's view of one capture channel.
type ChannelState ¶
type ChannelState string
ChannelState is the health of one capture channel. It is orthogonal to the meeting state: a meeting stays live when one of its two channels dies.
const ( ChannelStateIdle ChannelState = "idle" ChannelStateRecording ChannelState = "recording" ChannelStatePaused ChannelState = "paused" ChannelStateStalled ChannelState = "stalled" ChannelStateFailed ChannelState = "error" )
type Detection ¶
Detection is a call the user could take notes in.
func DetectCall ¶
func DetectCall(users []MicrophoneUser, allowlist []string) (Detection, bool)
DetectCall picks the call out of everything currently using the microphone.
The earliest-started match wins, so a browser that joined a call before a chat app opened its microphone is the one reported.
type EndWatcher ¶
type EndWatcher struct {
// Interval is how often the microphone is read.
Interval time.Duration
// Grace is how long the microphone must stay free of calling applications
// before the call is considered over.
Grace time.Duration
// Apps is the detection allowlist, same as Watcher's.
Apps []string
// Read returns the applications currently recording.
Read func() ([]MicrophoneUser, error)
// Recording reports whether a meeting is currently being recorded. The
// watcher only ever acts while this is true.
Recording func() bool
// End is called at most once per recording, when the call is over. It runs
// on the watch loop, so anything slow belongs in a goroutine.
End func()
// Now is overridable for tests.
Now func() time.Time
// contains filtered or unexported fields
}
EndWatcher turns a stream of microphone readings into "the call just ended".
It is the mirror of Watcher: where Watcher waits for a calling application to take the microphone, EndWatcher waits — only while a meeting is being recorded — for every calling application to let go of it and stay gone. The grace period matters more than the detection: calls drop the microphone for a few seconds when a headset reconnects or the user switches devices, and ending the recording on such a blip would truncate a meeting that is still going. A recording that runs a minute long is a small annoyance; one that cut off the decisions at the end of the meeting is useless.
func (*EndWatcher) Poll ¶
func (w *EndWatcher) Poll()
Poll performs one reading. Exported so the behaviour can be driven directly in tests without waiting on a ticker.
func (*EndWatcher) Watch ¶
func (w *EndWatcher) Watch(ctx context.Context)
Watch polls until the context ends.
type MicrophoneUser ¶
MicrophoneUser is one application recording right now, as reported by the host. It mirrors micuse.Session without binding this package to Windows.
type NotesBullet ¶
type NotesBullet struct {
Text string `json:"text"`
// SourceSegmentIDs are the transcript segments this bullet came from. A
// reader can follow them back to what was actually said; a bullet with none
// is one the model could not ground.
SourceSegmentIDs []int64 `json:"sourceSegmentIds,omitempty"`
// AnchorID names the user's own note this bullet came from. Bullets with an
// anchor are rendered from the stored note rather than from the model's
// paraphrase, so the user's words survive verbatim.
AnchorID string `json:"anchorId,omitempty"`
// Owner and Due are filled for action items where the conversation named
// them; both stay empty rather than being guessed.
Owner string `json:"owner,omitempty"`
Due string `json:"due,omitempty"`
}
NotesBullet is one line of the write-up.
type NotesDocument ¶
type NotesDocument struct {
TemplateSlug string `json:"templateSlug"`
Locale string `json:"locale,omitempty"`
ExecutiveBrief []string `json:"executiveBrief,omitempty"`
Sections []NotesSection `json:"sections"`
}
NotesDocument is the enhanced write-up of one meeting.
func (NotesDocument) ApplyAnchors ¶
func (d NotesDocument) ApplyAnchors(anchors []Anchor) NotesDocument
ApplyAnchors replaces the text of every anchored bullet with the note the user actually wrote.
This is enforcement, not trust: a model asked to preserve wording will usually comply and occasionally tidy it up, and a tidied-up note is no longer the user's note. Anchors that the model dropped entirely are appended to the first section, because a note the user took must appear somewhere.
func (NotesDocument) Finalize ¶
func (d NotesDocument) Finalize(locale string) NotesDocument
func (NotesDocument) HasContent ¶
func (d NotesDocument) HasContent() bool
HasContent reports whether the document says anything at all.
func (NotesDocument) Markdown ¶
func (d NotesDocument) Markdown() string
Markdown renders the document for copying, exporting and for the fallback path where a model could not produce structure.
func (NotesDocument) MarkdownDocument ¶
func (d NotesDocument) MarkdownDocument(title string, startedAt time.Time) string
MarkdownDocument renders the write-up as a complete Markdown file for copying and saving: a title, the date and language, the executive brief and then every section. Markdown() stays the body-only form the app stores and shows next to its own brief panel.
type NotesSection ¶
type NotesSection struct {
Slug string `json:"slug"`
Title string `json:"title"`
Bullets []NotesBullet `json:"bullets"`
}
NotesSection is one heading of the write-up.
type Options ¶
type Options struct {
NewPipeline PipelineFactory
// Log receives host-visible progress lines; kind is "info", "warn" or
// "error", matching the desktop log levels.
Log func(message, kind string)
// Now is overridable for tests.
Now func() time.Time
// DrainTimeout bounds how long Stop waits for in-flight transcription to
// land before the meeting is finished anyway.
DrainTimeout time.Duration
// DrainPoll is how often the drain wait re-checks. Tests shorten it.
DrainPoll time.Duration
// OnEnded fires once a meeting has finished and its transcript drain has
// either settled or been marked degraded.
//
// It is a direct call rather than a snapshot subscription because finishing
// a meeting — recording that it ended, writing it up — must not be
// best-effort: subscribers can miss a broadcast, and a meeting that ends
// without being recorded as ended looks to the user like it is still
// running. It runs on the caller's goroutine, so slow work belongs in one
// of its own.
OnEnded func(sessionID int64)
}
Options configure a Runtime.
type Pipeline ¶
type Pipeline interface {
// Channel names the capture source, one of the Channel* constants.
Channel() string
Start(opts speechkit.RecordingStartOptions) error
Stop(opts speechkit.RecordingStopOptions) error
// Events surfaces capture-device trouble (stalls, unplugs, driver errors)
// so the runtime can mark this channel degraded without killing the other.
Events() <-chan capturepkg.Event
Close() error
}
Pipeline is one capture channel: an audio session and the controller that turns its audio into transcripts. The host builds these, because device selection, provider wiring and the shared transcription worker are its concerns; the runtime only drives their lifecycle.
type PipelineFactory ¶
PipelineFactory opens the pipeline for one channel. Returning an error marks that channel unavailable; the meeting still runs on whatever opened.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime drives meeting capture. The zero value is not usable; call New.
func (*Runtime) ActiveSessionID ¶
ActiveSessionID returns the recording session being captured, or 0.
func (*Runtime) ElapsedMs ¶
ElapsedMs returns the session being captured and the current offset on its transcript timeline, in milliseconds. The offset is wall-clock time since the capture epoch — the same time base transcript segments are stamped with — so a screen capture taken now lands next to the words spoken now. Pauses do not stop this clock, because they do not stop the segment clock either. ok is false when nothing is being captured.
func (*Runtime) NoteSegmentCommitted ¶
NoteSegmentCommitted records that a submitted segment has been persisted.
func (*Runtime) NoteSegmentSubmitted ¶
NoteSegmentSubmitted records that a segment of this meeting entered the transcription queue, so Stop knows what it is waiting for.
func (*Runtime) Pause ¶
Pause stops capture on every channel but keeps the meeting and its shared timeline open, so Resume continues the same recording rather than a new one.
func (*Runtime) SetEndedHook ¶
SetEndedHook installs the terminal hook after construction, for hosts whose hook needs the runtime it belongs to.
func (*Runtime) SetPipelineFactory ¶
func (r *Runtime) SetPipelineFactory(factory PipelineFactory)
SetPipelineFactory installs the factory after construction. Hosts need this because the pipelines report their in-flight transcription back to the very runtime they belong to, so one of the two has to exist first.
func (*Runtime) Snapshot ¶
Snapshot returns the current runtime view. The zero SessionID with StateIdle means nothing is being recorded.
func (*Runtime) SnapshotFor ¶
SnapshotFor returns the runtime view for one session, or false when that session is not the one being recorded.
type Snapshot ¶
type Snapshot struct {
SessionID int64 `json:"sessionId"`
State State `json:"state"`
StartedAt time.Time `json:"startedAt,omitempty"`
Channels []ChannelSnapshot `json:"channels"`
PendingSegments int `json:"pendingSegments"`
// Degraded is true when capture ended with accepted transcript segments
// unresolved. PendingSegments reports how much transcript may be missing.
Degraded bool `json:"degraded"`
}
Snapshot is the runtime's view of capture right now.
type StartOptions ¶
type StartOptions struct {
// SessionID is the persisted recording session this capture belongs to.
SessionID int64
// Title is used for host-visible capture labels only.
Title string
Language string
// Channels to record. Defaults to microphone plus system loopback.
Channels []string
// Recording carries the transcription settings the host resolved for this
// meeting (provider stream, streaming segments, ...). The runtime fills in
// the session, channel and epoch fields per channel.
Recording speechkit.RecordingStartOptions
}
StartOptions describe the meeting to record.
type Template ¶
type Template struct {
Slug string `json:"slug"`
Name string `json:"name"`
// Prompt frames the meeting for the model: what this kind of conversation
// is and what a good write-up of it does.
Prompt string `json:"prompt"`
Sections []TemplateSection `json:"sections"`
}
Template describes what a kind of meeting needs written down.
func TemplateBySlug ¶
TemplateBySlug returns the named template, falling back to the general meeting template so an unknown or empty slug still produces notes.
type TemplateSection ¶
type TemplateSection struct {
Slug string `json:"slug"`
Title string `json:"title"`
Guidance string `json:"guidance"`
}
TemplateSection is one heading of the finished notes, with its own guidance.
type TranscriptLine ¶
type TranscriptLine struct {
SegmentID int64 `json:"segmentId"`
Speaker string `json:"speaker"`
Channel string `json:"channel"`
StartMs int64 `json:"startMs"`
Text string `json:"text"`
}
TranscriptLine is one segment of what was said, numbered so bullets can cite it.
func SplitLongLines ¶
func SplitLongLines(lines []TranscriptLine, maxRunes int) []TranscriptLine
SplitLongLines returns lines with every over-long line replaced by parts that fit MaxTranscriptLineRunes. Parts keep the segment id, speaker, channel and start time of the original, so citations still point at the segment and the parts stay in order.
func SuppressEcho ¶
func SuppressEcho(lines []TranscriptLine) []TranscriptLine
SuppressEcho drops the segments that are only the call arriving twice, keeping the loopback copy because it heard the call as digital audio.
It never touches what is stored: the transcript keeps both, because that is what was captured. Only the write-up is spared the duplicate.
type Watcher ¶
type Watcher struct {
// Interval is how often the microphone is read. Reading it is a handful of
// registry lookups, so this is cheap.
Interval time.Duration
// Rearm is how long an application must be off the microphone before a new
// call from it is announced.
Rearm time.Duration
// Apps is the detection allowlist.
Apps []string
// Read returns the applications currently recording.
Read func() ([]MicrophoneUser, error)
// Announce is called once per detected call. It runs on the watch loop, so
// anything slow belongs in a goroutine.
Announce func(Detection)
// Suspended reports whether detection should stand down — while a meeting is
// already being recorded, most obviously.
Suspended func() bool
// Now is overridable for tests.
Now func() time.Time
// contains filtered or unexported fields
}
Watcher turns a stream of microphone readings into "a call just started".
The debouncing matters more than the detection: a call that is announced twice, or announced again the moment the user declines, is worse than one that is missed. So a call is announced once, and the same application only becomes a candidate again after the microphone has been free for a while — which is what makes the second call of the day work without making the first one nag.