meeting

package
v0.69.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

View Source
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.

View Source
const (
	ChannelMicrophone = speechkit.CaptureChannelMicrophone
	ChannelSystem     = speechkit.CaptureChannelSystem
)

Channels a meeting records from.

View Source
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.

View Source
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

View Source
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")
)
View Source
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 CallApps

func CallApps(configured []string) []string

CallApps returns the detection allowlist, falling back to the built-in set.

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

func FormatOffset(ms int64) string

FormatOffset renders a point in the meeting as hh:mm:ss.

func MergeDigests

func MergeDigests(digests []string) (string, error)

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

func NormalizeDigestJSON(text string) (string, error)

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

func RepairTruncatedJSON(text string) (string, bool)

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 Anchor

type Anchor struct {
	ID   string `json:"id"`
	Text string `json:"text"`
	TsMs int64  `json:"tsMs"`
}

Anchor is one note the user wrote, with the point in the meeting it was written at.

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

type Detection struct {
	App   string
	Since time.Time
}

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

type MicrophoneUser struct {
	App   string
	Since time.Time
}

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

type PipelineFactory func(channel string) (Pipeline, error)

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 New

func New(opts Options) *Runtime

New creates a meeting runtime.

func (*Runtime) ActiveSessionID

func (r *Runtime) ActiveSessionID() int64

ActiveSessionID returns the recording session being captured, or 0.

func (*Runtime) ElapsedMs

func (r *Runtime) ElapsedMs() (sessionID int64, elapsedMs int64, ok bool)

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

func (r *Runtime) NoteSegmentCommitted(sessionID int64)

NoteSegmentCommitted records that a submitted segment has been persisted.

func (*Runtime) NoteSegmentSubmitted

func (r *Runtime) NoteSegmentSubmitted(sessionID int64)

NoteSegmentSubmitted records that a segment of this meeting entered the transcription queue, so Stop knows what it is waiting for.

func (*Runtime) Pause

func (r *Runtime) Pause() (Snapshot, error)

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) Resume

func (r *Runtime) Resume() (Snapshot, error)

Resume restarts capture on the channels that are still healthy.

func (*Runtime) SetEndedHook

func (r *Runtime) SetEndedHook(hook func(sessionID int64))

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

func (r *Runtime) Snapshot() Snapshot

Snapshot returns the current runtime view. The zero SessionID with StateIdle means nothing is being recorded.

func (*Runtime) SnapshotFor

func (r *Runtime) SnapshotFor(sessionID int64) (Snapshot, bool)

SnapshotFor returns the runtime view for one session, or false when that session is not the one being recorded.

func (*Runtime) Start

func (r *Runtime) Start(ctx context.Context, opts StartOptions) (Snapshot, error)

Start opens the capture channels and begins recording.

func (*Runtime) Stop

func (r *Runtime) Stop(ctx context.Context) (Snapshot, error)

Stop ends capture, waits for in-flight transcription to land, and closes the capture devices. The wait is bounded: a provider that never answers delays the meeting's end by DrainTimeout, it does not hang it.

func (*Runtime) Subscribe

func (r *Runtime) Subscribe() (<-chan Snapshot, func())

Subscribe returns a channel of snapshots plus a function that unsubscribes. Sends are non-blocking: a subscriber that stops reading misses intermediate states rather than stalling capture.

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.

func (Snapshot) Active

func (s Snapshot) Active() bool

Active reports whether this snapshot describes capture that is still running.

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 State

type State string

State is the lifecycle of one meeting capture.

const (
	StateIdle       State = "idle"
	StateStarting   State = "starting"
	StateLive       State = "live"
	StatePaused     State = "paused"
	StateFinalizing State = "finalizing"
	StateEnded      State = "ended"
)

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

func TemplateBySlug(slug string) Template

TemplateBySlug returns the named template, falling back to the general meeting template so an unknown or empty slug still produces notes.

func Templates

func Templates() []Template

Templates returns the built-in templates.

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.

func (*Watcher) Poll

func (w *Watcher) Poll()

Poll performs one reading. Exported so the behaviour can be driven directly in tests without waiting on a ticker.

func (*Watcher) Watch

func (w *Watcher) Watch(ctx context.Context)

Watch polls until the context ends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL