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 ¶
- Variables
- func EstimateContextTokens(convo *frames.LLMContext) int
- func EstimateTokens(text string) int
- func FormatMessagesForSummary(messages []frames.Message) string
- type AggregatedFrameSequencer
- func (s *AggregatedFrameSequencer) Clear()
- func (s *AggregatedFrameSequencer) CompleteSpokenSlot() []frames.Frame
- func (s *AggregatedFrameSequencer) Finalize(contextID string) []frames.Frame
- func (s *AggregatedFrameSequencer) Flush(lastWordPTS int64) []frames.Frame
- func (s *AggregatedFrameSequencer) ForceComplete(contextID string, lastWordPTS int64) []frames.Frame
- func (s *AggregatedFrameSequencer) ProcessWord(word string, pts int64, contextID string, includesInterFrame bool) []frames.Frame
- func (s *AggregatedFrameSequencer) RegisterSkipped(frame *frames.AggregatedTextFrame, contextID, transportDestination string) []frames.Frame
- func (s *AggregatedFrameSequencer) RegisterSpoken(frame *frames.AggregatedTextFrame, contextID, ttsText string, ...) []frames.Frame
- type CharAccumulator
- type MessagesToSummarize
- type SentenceTokenizer
- type TextSegmentMap
- func (m *TextSegmentMap) AdvanceWord(word string)
- func (m *TextSegmentMap) InTransformedSegment() bool
- func (m *TextSegmentMap) IsComplete() bool
- func (m *TextSegmentMap) LLMPos() int
- func (m *TextSegmentMap) LastCompletedSegment() (original string, ok bool)
- func (m *TextSegmentMap) LastLeadingDuplicate() int
- func (m *TextSegmentMap) LastOverflow() string
- func (m *TextSegmentMap) RawPos() int
- func (m *TextSegmentMap) Reset()
- func (m *TextSegmentMap) UserFacingPos() int
- func (m *TextSegmentMap) WordBelongsCurrentSegment(word string) bool
- type WordCompletionTracker
- func (t *WordCompletionTracker) AccumulatedRawText() (string, bool)
- func (t *WordCompletionTracker) AccumulatedTTSText() string
- func (t *WordCompletionTracker) AccumulatedUserFacingText() string
- func (t *WordCompletionTracker) AddWord(word string) bool
- func (t *WordCompletionTracker) FrameWord() (string, bool)
- func (t *WordCompletionTracker) IsComplete() bool
- func (t *WordCompletionTracker) OverflowWord() (string, bool)
- func (t *WordCompletionTracker) RawText() (string, bool)
- func (t *WordCompletionTracker) RemainingRawText() string
- func (t *WordCompletionTracker) RemainingRawTextOnly() (string, bool)
- func (t *WordCompletionTracker) RemainingTTSText(strip bool) string
- func (t *WordCompletionTracker) RemainingUserFacingText(strip bool) string
- func (t *WordCompletionTracker) Reset()
- func (t *WordCompletionTracker) Suppress() bool
- func (t *WordCompletionTracker) WordBelongsHere(word string) bool
- type WordTiming
Constants ¶
This section is empty.
Variables ¶
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 ¶
EstimateTokens estimates how many tokens text is, by the four-characters-per- token heuristic.
func FormatMessagesForSummary ¶
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 ¶
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 ¶
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 ¶
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.