context

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: BSD-2-Clause Imports: 11 Imported by: 0

Documentation

Overview

Package context provides the text-alignment utilities that let a TTS service map the words it actually spoke back to the original written text, so the conversation context can be truncated to exactly what was spoken when the bot is interrupted.

The three cooperating types are:

  • TextSegmentMap diffs the transformed text sent to a synthesizer against the original written text and, as spoken words stream in, advances a cursor through the original text. Unchanged spans advance proportionally; transformed spans (e.g. "$42.50" spoken as "forty two dollars and fifty cents") are held atomic and jump in one step once fully spoken.
  • WordCompletionTracker wraps a map for one aggregated text frame, reporting when the frame has been fully spoken and which span of original text each spoken word maps to.
  • MergePunctTokens normalizes raw word-timestamp streams.

All matching is purely textual: callers feed raw word-timestamp tokens and need not parse markup or punctuation themselves.

Index

Constants

This section is empty.

Variables

View Source
var ErrCharTimingLength = errors.New("context: character timing length mismatch")

ErrCharTimingLength is returned when a batch of character timings has a different number of characters and start offsets, which would misalign every word assembled after it.

Functions

func EstimateContextTokens

func EstimateContextTokens(convo *frames.LLMContext) int

EstimateContextTokens estimates the size of a conversation: every message's content, the tool calls it requested and the results it carries, plus the structural overhead each one costs.

A message written in one provider's own format is skipped: only that provider's adapter can read it, so nothing here can measure it.

func EstimateTokens

func EstimateTokens(text string) int

EstimateTokens estimates how many tokens text is, by the four-characters-per- token heuristic.

func FormatMessagesForSummary

func FormatMessagesForSummary(messages []frames.Message) string

FormatMessagesForSummary renders messages as the transcript handed to the model to summarize.

A message written in one provider's own format is left out: it holds internal data (a reasoning block, a thought signature) that is not meaningful as plain text, and the conversational content of that turn is carried by the ordinary assistant message beside it.

Types

type AggregatedFrameSequencer

type AggregatedFrameSequencer struct {
	// contains filtered or unexported fields
}

AggregatedFrameSequencer orders the frames of a synthesis so the conversation context is written in the order the text was spoken.

It holds a queue of spoken and skipped slots. A spoken slot is tracked by a WordCompletionTracker and completes as its words come back; a skipped slot waits until every spoken slot ahead of it is complete, then goes downstream.

Contexts can be live at once, so the state is kept in three tiers that never bleed into each other: slots is the single ordered timeline across all contexts, contextAppend marks a context live and says whether its words are written to the conversation, and streaming holds the transient pending sentence of a context still assembling one from tokens.

func NewAggregatedFrameSequencer

func NewAggregatedFrameSequencer(name string, streaming bool, tokenizer SentenceTokenizer) *AggregatedFrameSequencer

NewAggregatedFrameSequencer builds a sequencer labeled name.

Set streaming when each register call carries one token rather than a whole unit: tokens are then assembled back into sentences and a slot appears only once a boundary is confirmed. Streaming requires the caller to reuse one context id for a whole turn, since a sentence built from several tokens is registered under a single id and every one of its word timings must arrive tagged with that same id.

func (*AggregatedFrameSequencer) Clear

func (s *AggregatedFrameSequencer) Clear()

Clear drops every slot and all context state, for an interruption.

func (*AggregatedFrameSequencer) CompleteSpokenSlot

func (s *AggregatedFrameSequencer) CompleteSpokenSlot() []frames.Frame

CompleteSpokenSlot marks the active spoken slot complete. See completeSpokenSlotLocked.

func (*AggregatedFrameSequencer) Finalize

func (s *AggregatedFrameSequencer) Finalize(contextID string) []frames.Frame

Finalize closes out a context. See finalizeLocked.

func (*AggregatedFrameSequencer) Flush

func (s *AggregatedFrameSequencer) Flush(lastWordPTS int64) []frames.Frame

Flush returns every skipped frame now unblocked. See flushLocked.

func (*AggregatedFrameSequencer) ForceComplete

func (s *AggregatedFrameSequencer) ForceComplete(contextID string, lastWordPTS int64) []frames.Frame

ForceComplete completes a context's outstanding slots. See forceCompleteLocked.

func (*AggregatedFrameSequencer) ProcessWord

func (s *AggregatedFrameSequencer) ProcessWord(
	word string, pts int64, contextID string, includesInterFrame bool,
) []frames.Frame

ProcessWord records one spoken word. See processWordLocked.

func (*AggregatedFrameSequencer) RegisterSkipped

func (s *AggregatedFrameSequencer) RegisterSkipped(
	frame *frames.AggregatedTextFrame,
	contextID, transportDestination string,
) []frames.Frame

RegisterSkipped records a frame that is not spoken. See registerSkippedLocked.

func (*AggregatedFrameSequencer) RegisterSpoken

func (s *AggregatedFrameSequencer) RegisterSpoken(
	frame *frames.AggregatedTextFrame,
	contextID, ttsText string,
	appendToContext, buildTracker, includesInterFrame bool,
) []frames.Frame

RegisterSpoken records a frame handed to the synthesizer. See registerSpokenLocked.

type CharAccumulator

type CharAccumulator struct {
	// contains filtered or unexported fields
}

CharAccumulator assembles per-character timings into whole words. Some synthesizers report a start offset for every character (spaces and punctuation included) rather than per word, in batches that need not align with word boundaries. It splits on spaces, gives each word the offset of its first character, and carries a word split across two batches into the next one.

The zero value is ready to use. It is not safe for concurrent use; a synthesizer drives one per synthesis.

func (*CharAccumulator) Add

func (a *CharAccumulator) Add(chars []string, starts []float64) ([]WordTiming, error)

Add folds one batch in and returns every word it completes. chars and starts are parallel: starts[i] is when chars[i] begins, in seconds from the start of the synthesis. A word is completed by a space, so the last word of an utterance stays buffered until Flush.

func (*CharAccumulator) Flush

func (a *CharAccumulator) Flush() (WordTiming, bool)

Flush returns the word still being assembled, if any, and clears it. Call it when the synthesizer reports the utterance is complete: a final word has no terminating space to close it.

func (*CharAccumulator) Reset

func (a *CharAccumulator) Reset()

Reset discards any partially assembled word.

type MessagesToSummarize

type MessagesToSummarize struct {
	Messages            []frames.Message
	LastSummarizedIndex int
}

MessagesToSummarize is what GetMessagesToSummarize selected: the messages to fold into the summary, and the index of the last of them. LastSummarizedIndex is -1 when there is nothing to summarize.

func GetMessagesToSummarize

func GetMessagesToSummarize(convo *frames.LLMContext, minMessagesToKeep int) MessagesToSummarize

GetMessagesToSummarize selects the messages to fold into a summary, keeping out of it:

  • a system message at the head of the list, which frames the assistant's behavior and has to survive compression (jargo normally holds the system prompt outside the message list, where it is preserved anyway; this covers a conversation that carries one as a message);
  • the minMessagesToKeep most recent messages, which hold the immediate conversational context;
  • and everything from the first unanswered tool call onwards, so a request is never summarized away from the result answering it.

It reports no messages, and an index of -1, when there is nothing to summarize.

type SentenceTokenizer

type SentenceTokenizer interface {
	MatchEndOfSentence(text string) int
}

SentenceTokenizer finds sentence boundaries. It is the same contract the text package defines, restated here so this package does not depend on it.

type TextSegmentMap

type TextSegmentMap struct {
	// contains filtered or unexported fields
}

TextSegmentMap answers "where are we?" in three versions of one utterance, word by word.

A synthesizer reports the words it speaks. Each report has to be turned into a position, but into a position in three different strings, because the same utterance exists in three forms at once:

  • ttsText, what was actually spoken, tags and all: "Your balance is forty two dollars"
  • originalText, what a client displays: "Your balance is $42.50"
  • llmText, what the model wrote, so what the transcript should keep: "Your balance is <b>$42.50</b>". Defaults to originalText.

For a frame nothing rewrote, all three are the same string and every position is the same.

The hard part is that a spoken word need not appear in the other two. The synthesizer says "dollars"; nothing in "$42.50" matches it. So the map is built once, by diffing ttsText against originalText into aligned segments, each either survived unchanged or rewritten whole.

llmText is never compared against the others, and does not need to be. It holds the same letters and digits as originalText, in the same order, and differs only in what is wrapped around them: tags, delimiters, punctuation. So counting letters and digits is enough to keep it in step, and its cursor moves by that count.

From then on one real cursor moves: RawPos, how far into ttsText the synthesizer has got. UserFacingPos and LLMPos follow it. Through an unchanged segment they keep pace, word for word. Through a rewritten one they wait: there is no honest position halfway through "$42.50" while "forty two dollars" is being spoken, so they hold and then jump to the end of the span in one step when the last of its words lands.

Callers ask two things. WordBelongsCurrentSegment, does this token plausibly continue what is left to speak, and AdvanceWord, which consumes it. Both tolerate the ways synthesizers mangle tokens (added punctuation, changed case or diacritics, a fragment of a half-open tag) without the caller knowing anything about it; classifyHop holds that logic.

func NewTextSegmentMap

func NewTextSegmentMap(ttsText, originalText, llmText string) *TextSegmentMap

NewTextSegmentMap lines the three texts up against each other. The comparison happens once, here; everything after this only moves cursors.

ttsText is what was sent to the synthesizer, and so what incoming words are matched against; it may carry synthesis tags and rewritten values. originalText is the same content as a client displays it, before any rewriting, and is diffed against ttsText to build the segments. llmText is the same content as the model wrote it, which may add delimiters the other two never see; it rides its own cursor rather than being diffed. Pass "" for llmText to default it to originalText.

func (*TextSegmentMap) AdvanceWord

func (m *TextSegmentMap) AdvanceWord(word string)

AdvanceWord takes one spoken word and moves every cursor to where it ends.

Afterwards LastCompletedSegment, LastOverflow and LastLeadingDuplicate describe what this particular word did; each is cleared at the start of the next call.

The word may be a plain word, a word carrying its own spacing or punctuation, or a fragment of a half-open tag. Matching is textual, so the caller does not have to know which.

func (*TextSegmentMap) InTransformedSegment

func (m *TextSegmentMap) InTransformedSegment() bool

InTransformedSegment reports whether the cursor is partway through a rewritten segment.

func (*TextSegmentMap) IsComplete

func (m *TextSegmentMap) IsComplete() bool

IsComplete reports whether every letter and digit in the text has been spoken.

That is not the same as the cursor reaching the end. If all that is left is punctuation or tags, the text counts as finished even though those runes have not been walked over, because no word event is coming for them.

There is one exception. Punctuation separated from its word by a space, as French writes "Comment ça va ?", does arrive as its own word event, so the text stays unfinished until it does. Punctuation stuck to the word itself, as in "you?", was already taken with the word.

func (*TextSegmentMap) LLMPos

func (m *TextSegmentMap) LLMPos() int

LLMPos is how far into the LLM's text the spoken words have reached, as a rune offset.

func (*TextSegmentMap) LastCompletedSegment

func (m *TextSegmentMap) LastCompletedSegment() (original string, ok bool)

LastCompletedSegment returns the original text of the segment finished by the last AdvanceWord call, and whether one finished.

func (*TextSegmentMap) LastLeadingDuplicate

func (m *TextSegmentMap) LastLeadingDuplicate() int

LastLeadingDuplicate is how much of the last word's start was punctuation already spoken, in runes.

It is the opposite end of the word from LastOverflow: that one is about a tail running past this text, this one about a head repeating punctuation the previous word already took. Cut both off to get the part of the word that belongs to this frame.

func (*TextSegmentMap) LastOverflow

func (m *TextSegmentMap) LastOverflow() string

LastOverflow is the end of the last word passed to AdvanceWord, if it did not fit. It is "" most of the time, and is set only when that word ran past the end of the TTS text with no segment left to take the rest, which means the leftover belongs to the next frame. It is always the tail of the word that was passed in, so the part that did fit is the word minus this many trailing runes.

func (*TextSegmentMap) RawPos

func (m *TextSegmentMap) RawPos() int

RawPos is how far into the TTS text the synthesizer has spoken, counted from its start as a rune offset.

func (*TextSegmentMap) Reset

func (m *TextSegmentMap) Reset()

Reset puts every cursor back to the start of the text.

func (*TextSegmentMap) UserFacingPos

func (m *TextSegmentMap) UserFacingPos() int

UserFacingPos is how far into the user-facing text the spoken words have reached, as a rune offset.

func (*TextSegmentMap) WordBelongsCurrentSegment

func (m *TextSegmentMap) WordBelongsCurrentSegment(word string) bool

WordBelongsCurrentSegment reports whether word could be the next thing spoken here. It is AdvanceWord without the moving, so a caller can check first. A false answer means the synthesizer skipped ahead, and the word should go to the next frame instead.

A word with no letters or digits gets a second chance from symbolWordBelongs, since there is nothing in it to match on.

type WordCompletionTracker

type WordCompletionTracker struct {
	// contains filtered or unexported fields
}

WordCompletionTracker tracks whether all words of one aggregated text frame have been spoken, and maps each spoken word back to its span in the original text. It delegates cursor advancement to a TextSegmentMap built from the transformed ttsText: unchanged segments advance proportionally, transformed segments (e.g. "$42.50" spoken as "forty two dollars and fifty cents") are held atomic and jump to the end of the original span once fully spoken.

When llmText is provided, the tracker additionally maps each spoken word back to its corresponding span there, so callers can attach the original written text to per-word frames and the conversation context receives properly-formed content rather than the cleaned words the synthesizer reports.

func NewWordCompletionTracker

func NewWordCompletionTracker(ttsText, userFacingText, llmText string) *WordCompletionTracker

NewWordCompletionTracker builds a tracker for the frame being spoken. ttsText is the text sent to the synthesizer (may carry synthesis markup). userFacingText is the text as shown to the user; pass "" to default it to ttsText with markup stripped. llmText is the original model-produced text (with any delimiters); pass "" to disable original-text mapping.

func (*WordCompletionTracker) AccumulatedRawText

func (t *WordCompletionTracker) AccumulatedRawText() (string, bool)

AccumulatedRawText returns the original text consumed so far, and whether any original text was provided.

func (*WordCompletionTracker) AccumulatedTTSText

func (t *WordCompletionTracker) AccumulatedTTSText() string

AccumulatedTTSText returns the text sent to the synthesizer that has been consumed so far. Unlike FrameWord, which reflects only the last word, this is everything since construction or the last Reset.

func (*WordCompletionTracker) AccumulatedUserFacingText

func (t *WordCompletionTracker) AccumulatedUserFacingText() string

AccumulatedUserFacingText returns the user-facing text consumed so far.

func (*WordCompletionTracker) AddWord

func (t *WordCompletionTracker) AddWord(word string) bool

AddWord records one word the synthesizer reported speaking, and reports whether the frame is now fully spoken.

Three things can happen, in this order. The frame is already finished, and the word is ignored. The word does not match what is left to speak, so the synthesizer must have dropped an event: the frame is force-completed and this word is handed back as overflow. Otherwise the word advances the frame, and afterwards the accessors describe it: this frame's share of the word, the original text it stands for, and how much of the frame is now spoken.

func (*WordCompletionTracker) FrameWord

func (t *WordCompletionTracker) FrameWord() (string, bool)

FrameWord returns the portion of the last word belonging to this frame, whitespace-trimmed, and whether one was recorded.

func (*WordCompletionTracker) IsComplete

func (t *WordCompletionTracker) IsComplete() bool

IsComplete reports whether this frame's TTS text has been fully accounted for.

func (*WordCompletionTracker) OverflowWord

func (t *WordCompletionTracker) OverflowWord() (string, bool)

OverflowWord returns the raw suffix of the last word that overflows into the next frame, whitespace-trimmed, and whether there was overflow.

func (*WordCompletionTracker) RawText

func (t *WordCompletionTracker) RawText() (string, bool)

RawText returns the original-text span consumed for the last added word, whitespace-trimmed, and whether one was recorded. It is never set when no original text was provided, or for an intermediate word of a transformed segment.

func (*WordCompletionTracker) RemainingRawText

func (t *WordCompletionTracker) RemainingRawText() string

RemainingRawText returns the unspoken portion of the original text, trimmed. It is used to close out a frame so the context receives the full original text when the frame was not interrupted. Returns "" when nothing remains or no original text was provided.

func (*WordCompletionTracker) RemainingRawTextOnly

func (t *WordCompletionTracker) RemainingRawTextOnly() (string, bool)

RemainingRawTextOnly returns the unspoken portion of the original text, trimmed, and whether any original text was provided. It differs from RemainingRawText, which falls back to the user-facing text when there is none.

func (*WordCompletionTracker) RemainingTTSText

func (t *WordCompletionTracker) RemainingTTSText(strip bool) string

RemainingTTSText returns the unspoken portion of the text sent to the synthesizer. Leading whitespace is kept unless strip is set.

func (*WordCompletionTracker) RemainingUserFacingText

func (t *WordCompletionTracker) RemainingUserFacingText(strip bool) string

RemainingUserFacingText returns the unspoken portion of the user-facing text. Leading whitespace is kept unless strip is set, so accumulated plus remaining reconstructs the original exactly.

func (*WordCompletionTracker) Reset

func (t *WordCompletionTracker) Reset()

Reset returns the tracker to its initial state without changing the texts.

func (*WordCompletionTracker) Suppress

func (t *WordCompletionTracker) Suppress() bool

Suppress reports whether the last word is mid-flight inside a transformed segment. When true, the per-word frame must not be written to the context; only the completing word of the segment carries the original text.

func (*WordCompletionTracker) WordBelongsHere

func (t *WordCompletionTracker) WordBelongsHere(word string) bool

WordBelongsHere reports whether word plausibly belongs to the remaining TTS text of this frame. It is how a dropped word-timestamp event is detected: a word that does not match this frame's remaining content belongs to the next one, and this frame has to be force-completed.

type WordTiming

type WordTiming struct {
	Word   string
	Offset float64
}

WordTiming is a single word-timestamp event from a synthesizer: the spoken token and its start offset in seconds from the beginning of the synthesis.

func CharsAsWords

func CharsAsWords(chars []string, starts []float64) ([]WordTiming, error)

CharsAsWords treats each character as its own word, keeping only the ones carrying an alphanumeric. Languages written without spaces between words (Chinese and Japanese) get per-character timings that cannot be assembled into words by splitting on spaces, so they are reported as they arrive.

func MergePunctTokens

func MergePunctTokens(words []WordTiming) []WordTiming

MergePunctTokens merges punctuation- and space-only tokens into the preceding word. Some synthesizers emit spaces and punctuation as separate word-timestamp tokens rather than attaching them to the adjacent word; this collapses those so downstream consumers always receive words with trailing punctuation already attached.

A token is punct/space-only when it has no alphanumeric character after markup is stripped. Such a token is appended to the preceding word's text and its offset discarded (the preceding word's offset is kept). Leading punct/space tokens with no preceding word are dropped. Every returned token is trimmed of leading and trailing whitespace.

Jump to

Keyboard shortcuts

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